From 9dda678aebcac2980f7bb23a7a0ae6d3ef43fbeb Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 26 Dec 2021 11:24:34 +0100 Subject: [PATCH 01/66] Parse open EXR header --- src/ImageSharp/Configuration.cs | 11 +- .../Formats/Bmp/BmpArrayFileHeader.cs | 5 +- src/ImageSharp/Formats/Bmp/BmpConstants.cs | 4 +- src/ImageSharp/Formats/Bmp/BmpDecoder.cs | 2 - src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs | 2 +- src/ImageSharp/Formats/Bmp/BmpFormat.cs | 6 +- .../Formats/Bmp/BmpImageFormatDetector.cs | 5 +- src/ImageSharp/Formats/Bmp/BmpInfoHeader.cs | 20 +- .../Formats/OpenExr/ExrAttribute.cs | 26 ++ src/ImageSharp/Formats/OpenExr/ExrBox2i.cs | 24 ++ .../Formats/OpenExr/ExrChannelInfo.cs | 30 ++ .../Formats/OpenExr/ExrCompression.cs | 10 + .../Formats/OpenExr/ExrConfigurationModule.cs | 18 ++ .../Formats/OpenExr/ExrConstants.cs | 28 ++ src/ImageSharp/Formats/OpenExr/ExrDecoder.cs | 61 +++++ .../Formats/OpenExr/ExrDecoderCore.cs | 256 ++++++++++++++++++ src/ImageSharp/Formats/OpenExr/ExrFormat.cs | 38 +++ src/ImageSharp/Formats/OpenExr/ExrHeader.cs | 71 +++++ .../Formats/OpenExr/ExrImageFormatDetector.cs | 31 +++ .../Formats/OpenExr/ExrLineOrder.cs | 14 + src/ImageSharp/Formats/OpenExr/ExrMetadata.cs | 29 ++ .../Formats/OpenExr/ExrPixelType.cs | 23 ++ .../Formats/OpenExr/ExrThrowHelper.cs | 26 ++ .../Formats/OpenExr/IExrDecoderOptions.cs | 12 + src/ImageSharp/Formats/OpenExr/README.md | 4 + .../Pbm/BufferedReadStreamExtensions.cs | 26 ++ 26 files changed, 746 insertions(+), 36 deletions(-) create mode 100644 src/ImageSharp/Formats/OpenExr/ExrAttribute.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrBox2i.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrCompression.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrConstants.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrDecoder.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrFormat.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrHeader.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrImageFormatDetector.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrLineOrder.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrMetadata.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrPixelType.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs create mode 100644 src/ImageSharp/Formats/OpenExr/IExrDecoderOptions.cs create mode 100644 src/ImageSharp/Formats/OpenExr/README.md 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.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 @@ public int StreamProcessingBufferSize /// /// 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 @@ public void Configure(IConfigurationModule configuration) /// 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 @@ public void Configure(IConfigurationModule configuration) /// . /// . /// . + /// . /// /// 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 @@ public void Configure(IConfigurationModule configuration) 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 @@ public BmpArrayFileHeader(short type, int size, int offsetToNext, short width, s /// 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 @@ 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 @@ public BmpDecoderCore(Configuration configuration, IBmpDecoderOptions options) /// /// 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 @@ private BmpFormat() /// /// 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 @@ private BmpFormat() 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 @@ public sealed class BmpImageFormatDetector : IImageFormatDetector 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 @@ public BmpInfoHeader( /// 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 @@ public static BmpInfoHeader ParseCore(ReadOnlySpan data) /// 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 @@ public static BmpInfoHeader ParseOs22Short(ReadOnlySpan data) /// 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 @@ public static BmpInfoHeader ParseV3(ReadOnlySpan data) 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 @@ public static BmpInfoHeader ParseV3(ReadOnlySpan data) /// 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 @@ public static BmpInfoHeader ParseAdobeV3(ReadOnlySpan data, bool withAlpha 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 @@ public static int ReadDecimal(this BufferedReadStream stream) 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); + } } } From a27696abdbcf07b2ae4752840200f863ddebf42f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 28 Dec 2021 19:14:01 +0100 Subject: [PATCH 02/66] Decode uncompressed EXR images with 16 bit pixel data for each channel --- .../Formats/OpenExr/ExrDecoderCore.cs | 134 +++++++++++++++++- .../Pbm/BufferedReadStreamExtensions.cs | 28 +++- 2 files changed, 153 insertions(+), 9 deletions(-) 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 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals /// /// 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 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals /// private readonly IExrDecoderOptions options; + /// + /// The metadata. + /// + private ImageMetadata metadata; + /// /// Initializes a new instance of the class. /// @@ -58,12 +64,102 @@ public ExrDecoderCore(Configuration configuration, IExrDecoderOptions options) 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 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella 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 @@ private ExrHeader ParseHeader(BufferedReadStream stream) 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 @@ private ExrBox2i ReadBox2i(BufferedReadStream stream) 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 @@ private ExrChannelInfo ReadChannelInfo(BufferedReadStream stream, out int bytesR 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.IO; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Pbm { @@ -66,12 +67,12 @@ public static int ReadDecimal(this BufferedReadStream stream) } /// - /// 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 @@ public static float ReadSingle(this BufferedReadStream stream, Span scratc 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); + } } } From d3993f7bc0e4fd05a38e8174e140579c2ca53c17 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 28 Dec 2021 22:01:08 +0100 Subject: [PATCH 03/66] Decode EXR images with 32 bit float pixel data --- .../Formats/OpenExr/ExrCompression.cs | 2 +- .../Formats/OpenExr/ExrDecoderCore.cs | 72 +++++++++++++------ 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/ImageSharp/Formats/OpenExr/ExrCompression.cs b/src/ImageSharp/Formats/OpenExr/ExrCompression.cs index 3fa8cadfd9..bcc177bde3 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrCompression.cs @@ -3,7 +3,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr { - internal enum ExrCompression : int + internal enum ExrCompression { None = 0 } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index ea40857d8d..bbd7636a9e 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -66,19 +66,25 @@ public ExrDecoderCore(Configuration configuration, IExrDecoderOptions options) private IList Channels { get; set; } + private ExrCompression Compression { get; set; } + /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { ExrHeader header = this.ReadExrHeader(stream); + if (this.Compression != ExrCompression.None) + { + ExrThrowHelper.ThrowNotSupported("Only uncompressed EXR images are supported"); + } + 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); @@ -102,36 +108,52 @@ public Image Decode(BufferedReadStream stream, CancellationToken for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { - switch (this.Channels[channelIdx].ChannelName) + ExrChannelInfo channel = this.Channels[channelIdx]; + switch (channel.ChannelName) { case "R": - for (int x = 0; x < this.Width; x++) + switch (channel.PixelType) { - redPixelData[x] = stream.ReadHalfSingle(this.buffer); + case ExrPixelType.Half: + this.ReadPixelRowChannelHalfSingle(stream, redPixelData); + break; + case ExrPixelType.Float: + this.ReadPixelRowChannelSingle(stream, redPixelData); + break; } break; case "B": - for (int x = 0; x < this.Width; x++) + switch (channel.PixelType) { - bluePixelData[x] = stream.ReadHalfSingle(this.buffer); + case ExrPixelType.Half: + this.ReadPixelRowChannelHalfSingle(stream, bluePixelData); + break; + case ExrPixelType.Float: + this.ReadPixelRowChannelSingle(stream, bluePixelData); + break; } break; case "G": - for (int x = 0; x < this.Width; x++) + switch (channel.PixelType) { - greenPixelData[x] = stream.ReadHalfSingle(this.buffer); + case ExrPixelType.Half: + this.ReadPixelRowChannelHalfSingle(stream, greenPixelData); + break; + case ExrPixelType.Float: + this.ReadPixelRowChannelSingle(stream, greenPixelData); + break; } break; default: // Skip unknown channel. - // TODO: can we assume the same data size as the others here? - stream.Position += this.Width * 2; + int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.Uint ? 4 : 2; + stream.Position += this.Width * channelDataSizeInBytes; break; } } @@ -149,6 +171,22 @@ public Image Decode(BufferedReadStream stream, CancellationToken return image; } + private void ReadPixelRowChannelHalfSingle(BufferedReadStream stream, Span channelData) + { + for (int x = 0; x < this.Width; x++) + { + channelData[x] = stream.ReadHalfSingle(this.buffer); + } + } + + private void ReadPixelRowChannelSingle(BufferedReadStream stream, Span channelData) + { + for (int x = 0; x < this.Width; x++) + { + channelData[x] = stream.ReadSingle(this.buffer); + } + } + /// public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { @@ -182,14 +220,10 @@ private ExrHeader ReadExrHeader(BufferedReadStream stream) 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; this.Channels = header.Channels; + this.Compression = header.Compression.GetValueOrDefault(); this.metadata = new ImageMetadata(); @@ -210,13 +244,7 @@ private ExrHeader ParseHeader(BufferedReadStream stream) 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; + header.Compression = (ExrCompression)stream.ReadByte(); break; case "dataWindow": ExrBox2i dataWindow = this.ReadBox2i(stream); From aaf21434a82dc878981571d2461c7e430e072762 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 29 Dec 2021 11:02:43 +0100 Subject: [PATCH 04/66] 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 @@ public static Image Decode( 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 @@ 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; + 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 @@ public ExrDecoderCore(Configuration configuration, IExrDecoderOptions options) 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 @@ public Image Decode(BufferedReadStream stream, CancellationToken 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 @@ public Image Decode(BufferedReadStream stream, CancellationToken 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 @@ private void ReadPixelRowChannelSingle(BufferedReadStream stream, Span ch } } + 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 @@ private ExrHeader ReadExrHeader(BufferedReadStream stream) 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 @@ private ExrChannelInfo ReadChannelInfo(BufferedReadStream stream, out int bytesR 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})"); + } +} From 799dafe4140eee14c19ecad15fa1c9a8bc6a7eb8 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 31 Dec 2021 23:18:52 +0100 Subject: [PATCH 05/66] Encode EXR image with float pixel data --- src/ImageSharp/Formats/Bmp/BmpFileHeader.cs | 5 +- .../Formats/ImageExtensions.Save.cs | 104 ++++++ .../Formats/OpenExr/ExrConfigurationModule.cs | 1 + .../Formats/OpenExr/ExrDecoderCore.cs | 5 +- src/ImageSharp/Formats/OpenExr/ExrEncoder.cs | 38 +++ .../Formats/OpenExr/ExrEncoderCore.cs | 310 ++++++++++++++++++ src/ImageSharp/Formats/OpenExr/ExrMetadata.cs | 9 +- .../Formats/OpenExr/ExrPixelType.cs | 5 +- .../Formats/OpenExr/IExrEncoderOptions.cs | 16 + .../Formats/OpenExr/MetadataExtensions.cs | 21 ++ .../PixelImplementations/HalfVector4.cs | 59 +++- 11 files changed, 556 insertions(+), 17 deletions(-) create mode 100644 src/ImageSharp/Formats/OpenExr/ExrEncoder.cs create mode 100644 src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs create mode 100644 src/ImageSharp/Formats/OpenExr/IExrEncoderOptions.cs create mode 100644 src/ImageSharp/Formats/OpenExr/MetadataExtensions.cs diff --git a/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs b/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs index acbcdaef3a..ab56bd246b 100644 --- a/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs +++ b/src/ImageSharp/Formats/Bmp/BmpFileHeader.cs @@ -57,10 +57,7 @@ public BmpFileHeader(short type, int fileSize, int reserved, int offset) /// public int Offset { get; } - public static BmpFileHeader Parse(Span data) - { - return MemoryMarshal.Cast(data)[0]; - } + public static BmpFileHeader Parse(Span data) => MemoryMarshal.Cast(data)[0]; public void WriteTo(Span buffer) { diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.cs b/src/ImageSharp/Formats/ImageExtensions.Save.cs index a6a65aef62..315b942c64 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/ImageExtensions.Save.cs @@ -10,6 +10,7 @@ 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; @@ -847,5 +848,108 @@ public static Task SaveAsTiffAsync(this Image source, Stream stream, TiffEncoder encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TiffFormat.Instance), cancellationToken); + // foo + /// + /// Saves the image to the given stream with the Open Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsExr(this Image source, string path) => SaveAsExr(source, path, null); + + /// + /// Saves the image to the given stream with the Open Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, string path) => SaveAsExrAsync(source, path, null); + + /// + /// Saves the image to the given stream with the Open Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsExrAsync(source, path, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Open Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsExr(this Image source, string path, ExrEncoder encoder) => + source.Save( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ExrFormat.Instance)); + + /// + /// Saves the image to the given stream with the Open Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, string path, ExrEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + path, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ExrFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Open Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsExr(this Image source, Stream stream) + => SaveAsExr(source, stream, null); + + /// + /// Saves the image to the given stream with the Open Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsExrAsync(source, stream, null, cancellationToken); + + /// + /// Saves the image to the given stream with the Open Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static void SaveAsExr(this Image source, Stream stream, ExrEncoder encoder) + => source.Save( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ExrFormat.Instance)); + + /// + /// Saves the image to the given stream with the Open Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the stream is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, Stream stream, ExrEncoder encoder, CancellationToken cancellationToken = default) => + source.SaveAsync( + stream, + encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ExrFormat.Instance), + cancellationToken); } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs b/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs index ff0d3ee6a4..d5a03cd5dd 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs @@ -11,6 +11,7 @@ public sealed class ExrConfigurationModule : IConfigurationModule /// public void Configure(Configuration configuration) { + configuration.ImageFormatsManager.SetEncoder(ExrFormat.Instance, new ExrEncoder()); configuration.ImageFormatsManager.SetDecoder(ExrFormat.Instance, new ExrDecoder()); configuration.ImageFormatsManager.AddImageFormatDetector(new ExrImageFormatDetector()); } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index 79b117b736..b0424857ce 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -90,6 +90,9 @@ public Image Decode(BufferedReadStream stream, CancellationToken case ExrPixelType.Float: this.DecodeFloatingPointPixelData(stream, pixels); break; + case ExrPixelType.UnsignedInt: + this.DecodeUnsignedIntPixelData(stream, pixels); + break; default: ExrThrowHelper.ThrowNotSupported("Pixel type is not supported"); break; @@ -349,7 +352,7 @@ private ExrHeader ReadExrHeader(BufferedReadStream stream) } // Next three bytes contain info's about the image. - // We ignore those for now. + // TODO: We ignore those for now. stream.Read(this.buffer, 0, 3); ExrHeader header = this.ParseHeader(stream); diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoder.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoder.cs new file mode 100644 index 0000000000..71d69596a6 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoder.cs @@ -0,0 +1,38 @@ +// 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.Advanced; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + /// + /// Image encoder for writing an image to a stream in the OpenExr Format. + /// + public sealed class ExrEncoder : IImageEncoder, IExrEncoderOptions + { + /// + /// Gets or sets the pixel type of the image. + /// + public ExrPixelType? PixelType { get; set; } + + /// + public void Encode(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + var encoder = new ExrEncoderCore(this, image.GetMemoryAllocator()); + encoder.Encode(image, stream); + } + + /// + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + var encoder = new ExrEncoderCore(this, image.GetMemoryAllocator()); + return encoder.EncodeAsync(image, stream, cancellationToken); + } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs new file mode 100644 index 0000000000..35b381e9be --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs @@ -0,0 +1,310 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + /// + /// Image encoder for writing an image to a stream in the OpenExr format. + /// + internal sealed class ExrEncoderCore : IImageEncoderInternals + { + /// + /// Reusable buffer. + /// + private readonly byte[] buffer = new byte[8]; + + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The global configuration. + /// + private Configuration configuration; + + /// + /// The pixel type of the image. + /// + private ExrPixelType? pixelType; + + /// + /// Initializes a new instance of the class. + /// + /// The encoder options. + /// The memory manager. + public ExrEncoderCore(IExrEncoderOptions options, MemoryAllocator memoryAllocator) + { + this.memoryAllocator = memoryAllocator; + this.pixelType = options.PixelType; + } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + this.configuration = image.GetConfiguration(); + ImageMetadata metadata = image.Metadata; + ExrMetadata exrMetadata = metadata.GetExrMetadata(); + this.pixelType ??= exrMetadata.PixelType; + + int width = image.Width; + int height = image.Height; + ExrPixelType pixelType = ExrPixelType.Float; + var header = new ExrHeader() + { + Compression = ExrCompression.None, + AspectRatio = 1.0f, + DataWindow = new ExrBox2i(0, 0, width - 1, height - 1), + DisplayWindow = new ExrBox2i(0, 0, width - 1, height - 1), + LineOrder = ExrLineOrder.IncreasingY, + ScreenWindowCenter = new PointF(0.0f, 0.0f), + ScreenWindowWidth = 1, + Channels = new List() + { + new("B", pixelType, 0, 1, 1), + new("G", pixelType, 0, 1, 1), + new("R", pixelType, 0, 1, 1), + } + }; + + // Write magick bytes. + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, ExrConstants.MagickBytes); + stream.Write(this.buffer.AsSpan(0, 4)); + + // Version number. + this.buffer[0] = 2; + + // Second, third and fourth bytes store info about the image, all set to zero. + this.buffer[1] = 0; + this.buffer[2] = 0; + this.buffer[3] = 0; + stream.Write(this.buffer.AsSpan(0, 4)); + + // Write EXR header. + this.WriteHeader(stream, header); + + // Write pixel data. + Buffer2D pixels = image.Frames.RootFrame.PixelBuffer; + using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3); + Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); + Span blueBuffer = rgbBuffer.GetSpan().Slice(width, width); + Span greenBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); + + // Write offsets to each pixel row. + uint rowSizeBytes = (uint)(width * 3 * 4); + this.WriteRowOffsets(stream, height, rowSizeBytes); + for (int y = 0; y < height; y++) + { + Span pixelRowSpan = pixels.DangerousGetRowSpan(y); + + for (int x = 0; x < width; x++) + { + var vector4 = pixelRowSpan[x].ToVector4(); + redBuffer[x] = vector4.X; + greenBuffer[x] = vector4.Y; + blueBuffer[x] = vector4.Z; + } + + // Write row index. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); + stream.Write(this.buffer.AsSpan(0, 4)); + + // Write pixel row data size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, rowSizeBytes); + stream.Write(this.buffer.AsSpan(0, 4)); + + for (int x = 0; x < width; x++) + { + this.WriteSingle(stream, blueBuffer[x]); + } + + for (int x = 0; x < width; x++) + { + this.WriteSingle(stream, greenBuffer[x]); + } + + for (int x = 0; x < width; x++) + { + this.WriteSingle(stream, redBuffer[x]); + } + } + } + + private void WriteHeader(Stream stream, ExrHeader header) + { + this.WriteChannels(stream, header.Channels); + this.WriteCompression(stream, header.Compression.Value); + this.WriteDataWindow(stream, header.DataWindow.Value); + this.WriteDisplayWindow(stream, header.DisplayWindow.Value); + this.WritePixelAspectRatio(stream, header.AspectRatio.Value); + this.WriteLineOrder(stream, header.LineOrder.Value); + this.WriteScreenWindowCenter(stream, header.ScreenWindowCenter.Value); + this.WriteScreenWindowWidth(stream, header.ScreenWindowWidth.Value); + stream.WriteByte(0); + } + + private void WriteRowOffsets(Stream stream, int height, uint rowSizeBytes) + { + ulong startOfPixelData = (ulong)stream.Position + (8 * (ulong)height); + ulong offset = startOfPixelData; + for (int i = 0; i < height; i++) + { + BinaryPrimitives.WriteUInt64LittleEndian(this.buffer, offset); + stream.Write(this.buffer); + offset += 4 + 4 + rowSizeBytes; + } + } + + private void WriteChannels(Stream stream, IList channels) + { + int attributeSize = 0; + foreach (ExrChannelInfo channelInfo in channels) + { + attributeSize += channelInfo.ChannelName.Length + 1; + attributeSize += 16; + } + + // Last zero byte. + attributeSize++; + this.WriteAttributeInformation(stream, "channels", "chlist", attributeSize); + + foreach (ExrChannelInfo channelInfo in channels) + { + this.WriteChannelInfo(stream, channelInfo); + } + + // Last byte should be zero. + stream.WriteByte(0); + } + + private void WriteCompression(Stream stream, ExrCompression compression) + { + this.WriteAttributeInformation(stream, "compression", "compression", 1); + stream.WriteByte((byte)compression); + } + + private void WritePixelAspectRatio(Stream stream, float aspectRatio) + { + this.WriteAttributeInformation(stream, "pixelAspectRatio", "float", 4); + this.WriteSingle(stream, aspectRatio); + } + + private void WriteLineOrder(Stream stream, ExrLineOrder lineOrder) + { + this.WriteAttributeInformation(stream, "lineOrder", "lineOrder", 1); + stream.WriteByte((byte)lineOrder); + } + + private void WriteScreenWindowCenter(Stream stream, PointF screenWindowCenter) + { + this.WriteAttributeInformation(stream, "screenWindowCenter", "v2f", 8); + this.WriteSingle(stream, screenWindowCenter.X); + this.WriteSingle(stream, screenWindowCenter.Y); + } + + private void WriteScreenWindowWidth(Stream stream, float screenWindowWidth) + { + this.WriteAttributeInformation(stream, "screenWindowWidth", "float", 4); + this.WriteSingle(stream, screenWindowWidth); + } + + private void WriteDataWindow(Stream stream, ExrBox2i dataWindow) + { + this.WriteAttributeInformation(stream, "dataWindow", "box2i", 16); + this.WriteBox2i(stream, dataWindow); + } + + private void WriteDisplayWindow(Stream stream, ExrBox2i displayWindow) + { + this.WriteAttributeInformation(stream, "displayWindow", "box2i", 16); + this.WriteBox2i(stream, displayWindow); + } + + private void WriteAttributeInformation(Stream stream, string name, string type, int size) + { + // Write attribute name. + this.WriteString(stream, name); + + // Write attribute type. + this.WriteString(stream, type); + + // Write attribute size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)size); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + private void WriteChannelInfo(Stream stream, ExrChannelInfo channelInfo) + { + this.WriteString(stream, channelInfo.ChannelName); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, (int)channelInfo.PixelType); + stream.Write(this.buffer.AsSpan(0, 4)); + + stream.WriteByte(channelInfo.PLinear); + + // Next 3 bytes are reserved and will set to zero. + stream.WriteByte(0); + stream.WriteByte(0); + stream.WriteByte(0); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.XSampling); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.YSampling); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + private void WriteString(Stream stream, string str) + { + foreach (char c in str) + { + stream.WriteByte((byte)c); + } + + // Write termination byte. + stream.WriteByte(0); + } + + private void WriteBox2i(Stream stream, ExrBox2i box) + { + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMin); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMin); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMax); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMax); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + private unsafe void WriteSingle(Stream stream, float value) + { + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, *(int*)&value); + stream.Write(this.buffer.AsSpan(0, 4)); + } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs b/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs index f3723c5426..1e6eaf2bb6 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs @@ -19,9 +19,12 @@ public ExrMetadata() /// Initializes a new instance of the class. /// /// The metadata to create an instance from. - private ExrMetadata(ExrMetadata other) - { - } + private ExrMetadata(ExrMetadata other) => this.PixelType = other.PixelType; + + /// + /// Gets or sets the pixel format. + /// + public ExrPixelType PixelType { get; set; } = ExrPixelType.Half; /// public IDeepCloneable DeepClone() => new ExrMetadata(this); diff --git a/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs b/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs index fc3dc8ef9c..2a57afe988 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs @@ -3,7 +3,10 @@ namespace SixLabors.ImageSharp.Formats.OpenExr { - internal enum ExrPixelType + /// + /// The different pixel formats for a OpenEXR image. + /// + public enum ExrPixelType { /// /// unsigned int (32 bit). diff --git a/src/ImageSharp/Formats/OpenExr/IExrEncoderOptions.cs b/src/ImageSharp/Formats/OpenExr/IExrEncoderOptions.cs new file mode 100644 index 0000000000..938236b692 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/IExrEncoderOptions.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + /// + /// Configuration options for use during OpenExr encoding. + /// + internal interface IExrEncoderOptions + { + /// + /// Gets the pixel type of the image. + /// + ExrPixelType? PixelType { get; } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/MetadataExtensions.cs b/src/ImageSharp/Formats/OpenExr/MetadataExtensions.cs new file mode 100644 index 0000000000..24508f06a3 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/MetadataExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.OpenExr; +using SixLabors.ImageSharp.Metadata; + +namespace SixLabors.ImageSharp +{ + /// + /// Extension methods for the type. + /// + public static partial class MetadataExtensions + { + /// + /// Gets the open exr format specific metadata for the image. + /// + /// The metadata this method extends. + /// The . + public static ExrMetadata GetExrMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(ExrFormat.Instance); + } +} diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs index 94051e263e..3d4298d803 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs @@ -4,6 +4,7 @@ using System; using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.PixelFormats { @@ -13,8 +14,29 @@ namespace SixLabors.ImageSharp.PixelFormats /// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form. /// /// + [StructLayout(LayoutKind.Sequential)] public partial struct HalfVector4 : IPixel, IPackedVector { + /// + /// Gets or sets the red component. + /// + public float R; + + /// + /// Gets or sets the green component. + /// + public float G; + + /// + /// Gets or sets the blue component. + /// + public float B; + + /// + /// Gets or sets the alpha component. + /// + public float A; + /// /// Initializes a new instance of the struct. /// @@ -23,18 +45,43 @@ public partial struct HalfVector4 : IPixel, IPackedVector /// The z-component. /// The w-component. public HalfVector4(float x, float y, float z, float w) - : this(new Vector4(x, y, z, w)) { + this.R = x; + this.G = y; + this.B = z; + this.A = w; } /// /// Initializes a new instance of the struct. /// /// A vector containing the initial values for the components - public HalfVector4(Vector4 vector) => this.PackedValue = Pack(ref vector); + public HalfVector4(Vector4 vector) + { + this.R = vector.X; + this.G = vector.Y; + this.B = vector.Z; + this.A = vector.W; + } + + /// + /// Gets or sets the packed representation of the HalfVector4 struct. + /// + public ulong HalfVector + { + [MethodImpl(InliningOptions.ShortMethod)] + readonly get => Unsafe.As(ref Unsafe.AsRef(this)); + + [MethodImpl(InliningOptions.ShortMethod)] + set => Unsafe.As(ref this) = value; + } /// - public ulong PackedValue { get; set; } + public ulong PackedValue + { + readonly get => this.HalfVector; + set => this.HalfVector = value; + } /// /// Compares two objects for equality. @@ -86,11 +133,7 @@ public readonly Vector4 ToScaledVector4() /// [MethodImpl(InliningOptions.ShortMethod)] - public readonly Vector4 ToVector4() => new( - HalfTypeHelper.Unpack((ushort)this.PackedValue), - HalfTypeHelper.Unpack((ushort)(this.PackedValue >> 0x10)), - HalfTypeHelper.Unpack((ushort)(this.PackedValue >> 0x20)), - HalfTypeHelper.Unpack((ushort)(this.PackedValue >> 0x30))); + public readonly Vector4 ToVector4() => new(this.R, this.G, this.B, this.A); /// [MethodImpl(InliningOptions.ShortMethod)] From 4cf95a52df91bdf84c2bce158f1616fa29f1d87b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 1 Jan 2022 16:32:46 +0100 Subject: [PATCH 06/66] Encode EXR image with HalfSingle pixel data --- .../Formats/ImageExtensions.Save.cs | 25 ++--- src/ImageSharp/Formats/OpenExr/ExrBox2i.cs | 3 + .../Formats/OpenExr/ExrConstants.cs | 37 +++++++ .../Formats/OpenExr/ExrDecoderCore.cs | 16 +-- .../Formats/OpenExr/ExrEncoderCore.cs | 103 ++++++++++++------ src/ImageSharp/Formats/OpenExr/ExrMetadata.cs | 2 +- .../PixelImplementations/HalfVector4.cs | 59 ++-------- 7 files changed, 136 insertions(+), 109 deletions(-) diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.cs b/src/ImageSharp/Formats/ImageExtensions.Save.cs index 315b942c64..55fd00cf95 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/ImageExtensions.Save.cs @@ -848,14 +848,13 @@ public static Task SaveAsTiffAsync(this Image source, Stream stream, TiffEncoder encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TiffFormat.Instance), cancellationToken); - // foo /// /// Saves the image to the given stream with the Open Exr format. /// /// The image this method extends. /// The file path to save the image to. /// Thrown if the path is null. - public static void SaveAsExr(this Image source, string path) => SaveAsExr(source, path, null); + public static void SaveAsOpenExr(this Image source, string path) => SaveAsOpenExr(source, path, null); /// /// Saves the image to the given stream with the Open Exr format. @@ -864,7 +863,7 @@ public static Task SaveAsTiffAsync(this Image source, Stream stream, TiffEncoder /// The file path to save the image to. /// Thrown if the path is null. /// A representing the asynchronous operation. - public static Task SaveAsExrAsync(this Image source, string path) => SaveAsExrAsync(source, path, null); + public static Task SaveAsOpenExrAsync(this Image source, string path) => SaveAsOpenExrAsync(source, path, null); /// /// Saves the image to the given stream with the Open Exr format. @@ -874,8 +873,8 @@ public static Task SaveAsTiffAsync(this Image source, Stream stream, TiffEncoder /// The token to monitor for cancellation requests. /// Thrown if the path is null. /// A representing the asynchronous operation. - public static Task SaveAsExrAsync(this Image source, string path, CancellationToken cancellationToken) - => SaveAsExrAsync(source, path, null, cancellationToken); + public static Task SaveAsOpenExrAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsOpenExrAsync(source, path, null, cancellationToken); /// /// Saves the image to the given stream with the Open Exr format. @@ -884,7 +883,7 @@ public static Task SaveAsExrAsync(this Image source, string path, CancellationTo /// The file path to save the image to. /// The encoder to save the image with. /// Thrown if the path is null. - public static void SaveAsExr(this Image source, string path, ExrEncoder encoder) => + public static void SaveAsOpenExr(this Image source, string path, ExrEncoder encoder) => source.Save( path, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ExrFormat.Instance)); @@ -898,7 +897,7 @@ public static void SaveAsExr(this Image source, string path, ExrEncoder encoder) /// The token to monitor for cancellation requests. /// Thrown if the path is null. /// A representing the asynchronous operation. - public static Task SaveAsExrAsync(this Image source, string path, ExrEncoder encoder, CancellationToken cancellationToken = default) => + public static Task SaveAsOpenExrAsync(this Image source, string path, ExrEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( path, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ExrFormat.Instance), @@ -910,8 +909,8 @@ public static Task SaveAsExrAsync(this Image source, string path, ExrEncoder enc /// The image this method extends. /// The stream to save the image to. /// Thrown if the stream is null. - public static void SaveAsExr(this Image source, Stream stream) - => SaveAsExr(source, stream, null); + public static void SaveAsOpenExr(this Image source, Stream stream) + => SaveAsOpenExr(source, stream, null); /// /// Saves the image to the given stream with the Open Exr format. @@ -921,8 +920,8 @@ public static void SaveAsExr(this Image source, Stream stream) /// The token to monitor for cancellation requests. /// Thrown if the stream is null. /// A representing the asynchronous operation. - public static Task SaveAsExrAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) - => SaveAsExrAsync(source, stream, null, cancellationToken); + public static Task SaveAsOpenExrAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsOpenExrAsync(source, stream, null, cancellationToken); /// /// Saves the image to the given stream with the Open Exr format. @@ -932,7 +931,7 @@ public static Task SaveAsExrAsync(this Image source, Stream stream, Cancellation /// The encoder to save the image with. /// Thrown if the stream is null. /// A representing the asynchronous operation. - public static void SaveAsExr(this Image source, Stream stream, ExrEncoder encoder) + public static void SaveAsOpenExr(this Image source, Stream stream, ExrEncoder encoder) => source.Save( stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ExrFormat.Instance)); @@ -946,7 +945,7 @@ public static void SaveAsExr(this Image source, Stream stream, ExrEncoder encode /// The token to monitor for cancellation requests. /// Thrown if the stream is null. /// A representing the asynchronous operation. - public static Task SaveAsExrAsync(this Image source, Stream stream, ExrEncoder encoder, CancellationToken cancellationToken = default) => + public static Task SaveAsOpenExrAsync(this Image source, Stream stream, ExrEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( stream, encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ExrFormat.Instance), diff --git a/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs b/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs index 6fd610026b..89424b4259 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs @@ -1,8 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System.Diagnostics; + namespace SixLabors.ImageSharp.Formats.OpenExr { + [DebuggerDisplay("xMin: {XMin}, yMin: {YMin}, xMax: {XMax}, yMax: {YMax}")] internal struct ExrBox2i { public ExrBox2i(int xMin, int yMin, int xMax, int yMax) diff --git a/src/ImageSharp/Formats/OpenExr/ExrConstants.cs b/src/ImageSharp/Formats/OpenExr/ExrConstants.cs index b3f1a34245..40c34cce23 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrConstants.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrConstants.cs @@ -24,5 +24,42 @@ internal static class ExrConstants /// The magick bytes identifying an OpenExr image. /// public static readonly int MagickBytes = 20000630; + + /// + /// EXR attribute names. + /// + internal static class AttributeNames + { + public const string Channels = "channels"; + + public const string Compression = "compression"; + + public const string DataWindow = "dataWindow"; + + public const string DisplayWindow = "displayWindow"; + + public const string LineOrder = "lineOrder"; + + public const string PixelAspectRatio = "pixelAspectRatio"; + + public const string ScreenWindowCenter = "screenWindowCenter"; + + public const string ScreenWindowWidth = "screenWindowWidth"; + } + + internal static class AttibuteTypes + { + public const string ChannelList = "chlist"; + + public const string Compression = "compression"; + + public const string Float = "float"; + + public const string LineOrder = "lineOrder"; + + public const string TwoFloat = "v2f"; + + public const string BoxInt = "box2i"; + } } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index b0424857ce..46d3941f36 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -381,35 +381,35 @@ private ExrHeader ParseHeader(BufferedReadStream stream) { switch (attribute.Name) { - case "channels": + case ExrConstants.AttributeNames.Channels: IList channels = this.ReadChannelList(stream, attribute.Length); header.Channels = channels; break; - case "compression": + case ExrConstants.AttributeNames.Compression: header.Compression = (ExrCompression)stream.ReadByte(); break; - case "dataWindow": + case ExrConstants.AttributeNames.DataWindow: ExrBox2i dataWindow = this.ReadBox2i(stream); header.DataWindow = dataWindow; break; - case "displayWindow": + case ExrConstants.AttributeNames.DisplayWindow: ExrBox2i displayWindow = this.ReadBox2i(stream); header.DisplayWindow = displayWindow; break; - case "lineOrder": + case ExrConstants.AttributeNames.LineOrder: var lineOrder = (ExrLineOrder)stream.ReadByte(); header.LineOrder = lineOrder; break; - case "pixelAspectRatio": + case ExrConstants.AttributeNames.PixelAspectRatio: float aspectRatio = stream.ReadSingle(this.buffer); header.AspectRatio = aspectRatio; break; - case "screenWindowCenter": + case ExrConstants.AttributeNames.ScreenWindowCenter: float screenWindowCenterX = stream.ReadSingle(this.buffer); float screenWindowCenterY = stream.ReadSingle(this.buffer); header.ScreenWindowCenter = new PointF(screenWindowCenterX, screenWindowCenterY); break; - case "screenWindowWidth": + case ExrConstants.AttributeNames.ScreenWindowWidth: float screenWindowWidth = stream.ReadSingle(this.buffer); header.ScreenWindowWidth = screenWindowWidth; break; diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs index 35b381e9be..e3c4376708 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.IO; using System.Threading; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -29,11 +28,6 @@ internal sealed class ExrEncoderCore : IImageEncoderInternals /// private readonly MemoryAllocator memoryAllocator; - /// - /// The global configuration. - /// - private Configuration configuration; - /// /// The pixel type of the image. /// @@ -63,14 +57,11 @@ public void Encode(Image image, Stream stream, CancellationToken Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - this.configuration = image.GetConfiguration(); ImageMetadata metadata = image.Metadata; ExrMetadata exrMetadata = metadata.GetExrMetadata(); this.pixelType ??= exrMetadata.PixelType; - int width = image.Width; int height = image.Height; - ExrPixelType pixelType = ExrPixelType.Float; var header = new ExrHeader() { Compression = ExrCompression.None, @@ -82,9 +73,9 @@ public void Encode(Image image, Stream stream, CancellationToken ScreenWindowWidth = 1, Channels = new List() { - new("B", pixelType, 0, 1, 1), - new("G", pixelType, 0, 1, 1), - new("R", pixelType, 0, 1, 1), + new("B", this.pixelType.Value, 0, 1, 1), + new("G", this.pixelType.Value, 0, 1, 1), + new("R", this.pixelType.Value, 0, 1, 1), } }; @@ -112,7 +103,9 @@ public void Encode(Image image, Stream stream, CancellationToken Span greenBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); // Write offsets to each pixel row. - uint rowSizeBytes = (uint)(width * 3 * 4); + int bytesPerPixel = this.pixelType == ExrPixelType.Half ? 2 : 4; + int numberOfChannels = 3; + uint rowSizeBytes = (uint)(width * numberOfChannels * bytesPerPixel); this.WriteRowOffsets(stream, height, rowSizeBytes); for (int y = 0; y < height; y++) { @@ -134,19 +127,14 @@ public void Encode(Image image, Stream stream, CancellationToken BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, rowSizeBytes); stream.Write(this.buffer.AsSpan(0, 4)); - for (int x = 0; x < width; x++) - { - this.WriteSingle(stream, blueBuffer[x]); - } - - for (int x = 0; x < width; x++) + switch (this.pixelType) { - this.WriteSingle(stream, greenBuffer[x]); - } - - for (int x = 0; x < width; x++) - { - this.WriteSingle(stream, redBuffer[x]); + case ExrPixelType.Float: + this.WriteSingleRow(stream, width, blueBuffer, greenBuffer, redBuffer); + break; + case ExrPixelType.Half: + this.WriteHalfSingleRow(stream, width, blueBuffer, greenBuffer, redBuffer); + break; } } } @@ -164,6 +152,42 @@ private void WriteHeader(Stream stream, ExrHeader header) stream.WriteByte(0); } + private void WriteSingleRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + for (int x = 0; x < width; x++) + { + this.WriteSingle(stream, blueBuffer[x]); + } + + for (int x = 0; x < width; x++) + { + this.WriteSingle(stream, greenBuffer[x]); + } + + for (int x = 0; x < width; x++) + { + this.WriteSingle(stream, redBuffer[x]); + } + } + + private void WriteHalfSingleRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + for (int x = 0; x < width; x++) + { + this.WriteHalfSingle(stream, blueBuffer[x]); + } + + for (int x = 0; x < width; x++) + { + this.WriteHalfSingle(stream, greenBuffer[x]); + } + + for (int x = 0; x < width; x++) + { + this.WriteHalfSingle(stream, redBuffer[x]); + } + } + private void WriteRowOffsets(Stream stream, int height, uint rowSizeBytes) { ulong startOfPixelData = (ulong)stream.Position + (8 * (ulong)height); @@ -187,7 +211,7 @@ private void WriteChannels(Stream stream, IList channels) // Last zero byte. attributeSize++; - this.WriteAttributeInformation(stream, "channels", "chlist", attributeSize); + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Channels, ExrConstants.AttibuteTypes.ChannelList, attributeSize); foreach (ExrChannelInfo channelInfo in channels) { @@ -200,45 +224,45 @@ private void WriteChannels(Stream stream, IList channels) private void WriteCompression(Stream stream, ExrCompression compression) { - this.WriteAttributeInformation(stream, "compression", "compression", 1); + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Compression, ExrConstants.AttibuteTypes.Compression, 1); stream.WriteByte((byte)compression); } private void WritePixelAspectRatio(Stream stream, float aspectRatio) { - this.WriteAttributeInformation(stream, "pixelAspectRatio", "float", 4); + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.PixelAspectRatio, ExrConstants.AttibuteTypes.Float, 4); this.WriteSingle(stream, aspectRatio); } private void WriteLineOrder(Stream stream, ExrLineOrder lineOrder) { - this.WriteAttributeInformation(stream, "lineOrder", "lineOrder", 1); + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.LineOrder, ExrConstants.AttibuteTypes.LineOrder, 1); stream.WriteByte((byte)lineOrder); } private void WriteScreenWindowCenter(Stream stream, PointF screenWindowCenter) { - this.WriteAttributeInformation(stream, "screenWindowCenter", "v2f", 8); + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowCenter, ExrConstants.AttibuteTypes.TwoFloat, 8); this.WriteSingle(stream, screenWindowCenter.X); this.WriteSingle(stream, screenWindowCenter.Y); } private void WriteScreenWindowWidth(Stream stream, float screenWindowWidth) { - this.WriteAttributeInformation(stream, "screenWindowWidth", "float", 4); + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowWidth, ExrConstants.AttibuteTypes.Float, 4); this.WriteSingle(stream, screenWindowWidth); } private void WriteDataWindow(Stream stream, ExrBox2i dataWindow) { - this.WriteAttributeInformation(stream, "dataWindow", "box2i", 16); - this.WriteBox2i(stream, dataWindow); + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DataWindow, ExrConstants.AttibuteTypes.BoxInt, 16); + this.WriteBoxInteger(stream, dataWindow); } private void WriteDisplayWindow(Stream stream, ExrBox2i displayWindow) { - this.WriteAttributeInformation(stream, "displayWindow", "box2i", 16); - this.WriteBox2i(stream, displayWindow); + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DisplayWindow, ExrConstants.AttibuteTypes.BoxInt, 16); + this.WriteBoxInteger(stream, displayWindow); } private void WriteAttributeInformation(Stream stream, string name, string type, int size) @@ -286,7 +310,7 @@ private void WriteString(Stream stream, string str) stream.WriteByte(0); } - private void WriteBox2i(Stream stream, ExrBox2i box) + private void WriteBoxInteger(Stream stream, ExrBox2i box) { BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMin); stream.Write(this.buffer.AsSpan(0, 4)); @@ -306,5 +330,12 @@ private unsafe void WriteSingle(Stream stream, float value) BinaryPrimitives.WriteInt32LittleEndian(this.buffer, *(int*)&value); stream.Write(this.buffer.AsSpan(0, 4)); } + + private void WriteHalfSingle(Stream stream, float value) + { + ushort valueAsShort = HalfTypeHelper.Pack(value); + BinaryPrimitives.WriteUInt16LittleEndian(this.buffer, valueAsShort); + stream.Write(this.buffer.AsSpan(0, 2)); + } } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs b/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs index 1e6eaf2bb6..7af01a38f0 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs @@ -24,7 +24,7 @@ public ExrMetadata() /// /// Gets or sets the pixel format. /// - public ExrPixelType PixelType { get; set; } = ExrPixelType.Half; + public ExrPixelType PixelType { get; set; } = ExrPixelType.Float; /// public IDeepCloneable DeepClone() => new ExrMetadata(this); diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs index 3d4298d803..94051e263e 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/HalfVector4.cs @@ -4,7 +4,6 @@ using System; using System.Numerics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.PixelFormats { @@ -14,29 +13,8 @@ namespace SixLabors.ImageSharp.PixelFormats /// Ranges from [-1, -1, -1, -1] to [1, 1, 1, 1] in vector form. /// /// - [StructLayout(LayoutKind.Sequential)] public partial struct HalfVector4 : IPixel, IPackedVector { - /// - /// Gets or sets the red component. - /// - public float R; - - /// - /// Gets or sets the green component. - /// - public float G; - - /// - /// Gets or sets the blue component. - /// - public float B; - - /// - /// Gets or sets the alpha component. - /// - public float A; - /// /// Initializes a new instance of the struct. /// @@ -45,43 +23,18 @@ public partial struct HalfVector4 : IPixel, IPackedVector /// The z-component. /// The w-component. public HalfVector4(float x, float y, float z, float w) + : this(new Vector4(x, y, z, w)) { - this.R = x; - this.G = y; - this.B = z; - this.A = w; } /// /// Initializes a new instance of the struct. /// /// A vector containing the initial values for the components - public HalfVector4(Vector4 vector) - { - this.R = vector.X; - this.G = vector.Y; - this.B = vector.Z; - this.A = vector.W; - } - - /// - /// Gets or sets the packed representation of the HalfVector4 struct. - /// - public ulong HalfVector - { - [MethodImpl(InliningOptions.ShortMethod)] - readonly get => Unsafe.As(ref Unsafe.AsRef(this)); - - [MethodImpl(InliningOptions.ShortMethod)] - set => Unsafe.As(ref this) = value; - } + public HalfVector4(Vector4 vector) => this.PackedValue = Pack(ref vector); /// - public ulong PackedValue - { - readonly get => this.HalfVector; - set => this.HalfVector = value; - } + public ulong PackedValue { get; set; } /// /// Compares two objects for equality. @@ -133,7 +86,11 @@ public readonly Vector4 ToScaledVector4() /// [MethodImpl(InliningOptions.ShortMethod)] - public readonly Vector4 ToVector4() => new(this.R, this.G, this.B, this.A); + public readonly Vector4 ToVector4() => new( + HalfTypeHelper.Unpack((ushort)this.PackedValue), + HalfTypeHelper.Unpack((ushort)(this.PackedValue >> 0x10)), + HalfTypeHelper.Unpack((ushort)(this.PackedValue >> 0x20)), + HalfTypeHelper.Unpack((ushort)(this.PackedValue >> 0x30))); /// [MethodImpl(InliningOptions.ShortMethod)] From 7e7472d575d6510b3d8cca1d7c4ebf115caa2d9a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 1 Jan 2022 18:09:31 +0100 Subject: [PATCH 07/66] Encode EXR image with usigned int pixel data --- .../Formats/OpenExr/ExrChannelInfo.cs | 2 + .../Formats/OpenExr/ExrDecoderCore.cs | 14 +-- .../Formats/OpenExr/ExrEncoderCore.cs | 98 +++++++++++++++++-- src/ImageSharp/Image.Decode.cs | 2 - 4 files changed, 98 insertions(+), 18 deletions(-) diff --git a/src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs b/src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs index 81b40ea66f..28a014853d 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs @@ -1,10 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System.Diagnostics; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Formats.OpenExr { + [DebuggerDisplay("Name: {ChannelName}, PixelType: {PixelType}")] [StructLayout(LayoutKind.Sequential, Pack = 1)] internal readonly struct ExrChannelInfo { diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index 46d3941f36..053adbd074 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -106,8 +106,8 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf { 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); + Span greenPixelData = rowBuffer.GetSpan().Slice(this.Width, this.Width); + Span bluePixelData = rowBuffer.GetSpan().Slice(this.Width * 2, this.Width); TPixel color = default; for (int y = 0; y < this.Height; y++) @@ -193,8 +193,8 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe { 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); + Span greenPixelData = rowBuffer.GetSpan().Slice(this.Width, this.Width); + Span bluePixelData = rowBuffer.GetSpan().Slice(this.Width * 2, this.Width); TPixel color = default; for (int y = 0; y < this.Height; y++) @@ -389,11 +389,11 @@ private ExrHeader ParseHeader(BufferedReadStream stream) header.Compression = (ExrCompression)stream.ReadByte(); break; case ExrConstants.AttributeNames.DataWindow: - ExrBox2i dataWindow = this.ReadBox2i(stream); + ExrBox2i dataWindow = this.ReadBoxInteger(stream); header.DataWindow = dataWindow; break; case ExrConstants.AttributeNames.DisplayWindow: - ExrBox2i displayWindow = this.ReadBox2i(stream); + ExrBox2i displayWindow = this.ReadBoxInteger(stream); header.DisplayWindow = displayWindow; break; case ExrConstants.AttributeNames.LineOrder: @@ -441,7 +441,7 @@ private ExrAttribute ReadAttribute(BufferedReadStream stream) return new ExrAttribute(attributeName, attributeType, attributeSize); } - private ExrBox2i ReadBox2i(BufferedReadStream stream) + private ExrBox2i ReadBoxInteger(BufferedReadStream stream) { stream.Read(this.buffer, 0, 4); int xMin = BinaryPrimitives.ReadInt32LittleEndian(this.buffer); diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs index e3c4376708..221877a063 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs @@ -6,6 +6,7 @@ using System.Buffers.Binary; using System.Collections.Generic; using System.IO; +using System.Runtime.CompilerServices; using System.Threading; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -57,6 +58,8 @@ public void Encode(Image image, Stream stream, CancellationToken Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); + Buffer2D pixels = image.Frames.RootFrame.PixelBuffer; + ImageMetadata metadata = image.Metadata; ExrMetadata exrMetadata = metadata.GetExrMetadata(); this.pixelType ??= exrMetadata.PixelType; @@ -86,7 +89,7 @@ public void Encode(Image image, Stream stream, CancellationToken // Version number. this.buffer[0] = 2; - // Second, third and fourth bytes store info about the image, all set to zero. + // Second, third and fourth bytes store info about the image, set all to default: zero. this.buffer[1] = 0; this.buffer[2] = 0; this.buffer[3] = 0; @@ -95,18 +98,33 @@ public void Encode(Image image, Stream stream, CancellationToken // Write EXR header. this.WriteHeader(stream, header); + // Write offsets to each pixel row. + int bytesPerChannel = this.pixelType == ExrPixelType.Half ? 2 : 4; + int numberOfChannels = 3; + uint rowSizeBytes = (uint)(width * numberOfChannels * bytesPerChannel); + this.WriteRowOffsets(stream, height, rowSizeBytes); + // Write pixel data. - Buffer2D pixels = image.Frames.RootFrame.PixelBuffer; + switch (this.pixelType) + { + case ExrPixelType.Half: + case ExrPixelType.Float: + this.EncodeFloatingPointPixelData(stream, pixels, width, height, rowSizeBytes); + break; + case ExrPixelType.UnsignedInt: + this.EncodeUnsignedIntPixelData(stream, pixels, width, height, rowSizeBytes); + break; + } + } + + private void EncodeFloatingPointPixelData(Stream stream, Buffer2D pixels, int width, int height, uint rowSizeBytes) + where TPixel : unmanaged, IPixel + { using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3); Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); - Span blueBuffer = rgbBuffer.GetSpan().Slice(width, width); - Span greenBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); + Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); + Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); - // Write offsets to each pixel row. - int bytesPerPixel = this.pixelType == ExrPixelType.Half ? 2 : 4; - int numberOfChannels = 3; - uint rowSizeBytes = (uint)(width * numberOfChannels * bytesPerPixel); - this.WriteRowOffsets(stream, height, rowSizeBytes); for (int y = 0; y < height; y++) { Span pixelRowSpan = pixels.DangerousGetRowSpan(y); @@ -139,6 +157,41 @@ public void Encode(Image image, Stream stream, CancellationToken } } + private void EncodeUnsignedIntPixelData(Stream stream, Buffer2D pixels, int width, int height, uint rowSizeBytes) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3); + Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); + Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); + Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); + + var rgb = default(Rgb96); + for (int y = 0; y < height; y++) + { + Span pixelRowSpan = pixels.DangerousGetRowSpan(y); + + for (int x = 0; x < width; x++) + { + var vector4 = pixelRowSpan[x].ToVector4(); + rgb.FromVector4(vector4); + + redBuffer[x] = rgb.R; + greenBuffer[x] = rgb.G; + blueBuffer[x] = rgb.B; + } + + // Write row index. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); + stream.Write(this.buffer.AsSpan(0, 4)); + + // Write pixel row data size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, rowSizeBytes); + stream.Write(this.buffer.AsSpan(0, 4)); + + this.WriteUnsignedIntRow(stream, width, blueBuffer, greenBuffer, redBuffer); + } + } + private void WriteHeader(Stream stream, ExrHeader header) { this.WriteChannels(stream, header.Channels); @@ -188,6 +241,24 @@ private void WriteHalfSingleRow(Stream stream, int width, Span blueBuffer } } + private void WriteUnsignedIntRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + for (int x = 0; x < width; x++) + { + this.WriteUnsignedInt(stream, blueBuffer[x]); + } + + for (int x = 0; x < width; x++) + { + this.WriteUnsignedInt(stream, greenBuffer[x]); + } + + for (int x = 0; x < width; x++) + { + this.WriteUnsignedInt(stream, redBuffer[x]); + } + } + private void WriteRowOffsets(Stream stream, int height, uint rowSizeBytes) { ulong startOfPixelData = (ulong)stream.Position + (8 * (ulong)height); @@ -325,17 +396,26 @@ private void WriteBoxInteger(Stream stream, ExrBox2i box) stream.Write(this.buffer.AsSpan(0, 4)); } + [MethodImpl(InliningOptions.ShortMethod)] private unsafe void WriteSingle(Stream stream, float value) { BinaryPrimitives.WriteInt32LittleEndian(this.buffer, *(int*)&value); stream.Write(this.buffer.AsSpan(0, 4)); } + [MethodImpl(InliningOptions.ShortMethod)] private void WriteHalfSingle(Stream stream, float value) { ushort valueAsShort = HalfTypeHelper.Pack(value); BinaryPrimitives.WriteUInt16LittleEndian(this.buffer, valueAsShort); stream.Write(this.buffer.AsSpan(0, 2)); } + + [MethodImpl(InliningOptions.ShortMethod)] + private void WriteUnsignedInt(Stream stream, uint value) + { + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, value); + stream.Write(this.buffer.AsSpan(0, 4)); + } } } diff --git a/src/ImageSharp/Image.Decode.cs b/src/ImageSharp/Image.Decode.cs index ee340bf86e..3e1ffe9baf 100644 --- a/src/ImageSharp/Image.Decode.cs +++ b/src/ImageSharp/Image.Decode.cs @@ -2,9 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; -using System.Buffers; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; From 652b3855c40b5adde164dd1ec0d29f5fa4141cbc Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 2 Jan 2022 18:03:19 +0100 Subject: [PATCH 08/66] Update tests for EXR images --- .../PixelImplementations/Rgb96.cs | 11 +- tests/ImageSharp.Tests/ConfigurationTests.cs | 3 +- .../Formats/Exr/ImageExtensionsTest.cs | 156 ++++++++++++++++++ .../Formats/GeneralFormatTests.cs | 18 +- 4 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs index 405ed40a14..83333a377e 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs @@ -18,7 +18,9 @@ namespace SixLabors.ImageSharp.PixelFormats [StructLayout(LayoutKind.Sequential)] public partial struct Rgb96 : IPixel { - private const float Max = uint.MaxValue; + private const float InvMax = 1.0f / uint.MaxValue; + + private const double Max = uint.MaxValue; /// /// Gets or sets the red component. @@ -86,6 +88,7 @@ public Rgb96(uint r, uint g, uint b) [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); @@ -94,9 +97,9 @@ public void FromVector4(Vector4 vector) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector4 ToVector4() => new( - this.R / Max, - this.G / Max, - this.B / Max, + this.R * InvMax, + this.G * InvMax, + this.B * InvMax, 1.0f); /// diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index 7497e1b4cc..eda8626b13 100644 --- a/tests/ImageSharp.Tests/ConfigurationTests.cs +++ b/tests/ImageSharp.Tests/ConfigurationTests.cs @@ -8,7 +8,6 @@ using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.Tests.Memory; using Xunit; // ReSharper disable InconsistentNaming @@ -23,7 +22,7 @@ public class ConfigurationTests public Configuration DefaultConfiguration { get; } - private readonly int expectedDefaultConfigurationCount = 8; + private readonly int expectedDefaultConfigurationCount = 9; public ConfigurationTests() { diff --git a/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs new file mode 100644 index 0000000000..9ff506efad --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs @@ -0,0 +1,156 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Threading.Tasks; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.OpenExr; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Formats.Exr +{ + [Trait("Format", "Exr")] + public class ImageExtensionsTest + { + [Fact] + public void SaveAsExr_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsExr_Path.exr"); + + using (var image = new Image(10, 10)) + { + image.SaveAsOpenExr(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/x-exr", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsExrAsync_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsExrAsync_Path.exr"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsOpenExrAsync(file); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/x-exr", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsExr_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsExr_Path_Encoder.exr"); + + using (var image = new Image(10, 10)) + { + image.SaveAsOpenExr(file, new ExrEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/x-exr", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsExrAsync_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsExrAsync_Path_Encoder.tiff"); + + using (var image = new Image(10, 10)) + { + await image.SaveAsOpenExrAsync(file, new ExrEncoder()); + } + + using (Image.Load(file, out IImageFormat mime)) + { + Assert.Equal("image/x-exr", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsExr_Stream() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsOpenExr(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/x-exr", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsExrAsync_StreamAsync() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsOpenExrAsync(memoryStream); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/x-exr", mime.DefaultMimeType); + } + } + + [Fact] + public void SaveAsExr_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + image.SaveAsOpenExr(memoryStream, new ExrEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/x-exr", mime.DefaultMimeType); + } + } + + [Fact] + public async Task SaveAsExrAsync_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); + + using (var image = new Image(10, 10)) + { + await image.SaveAsOpenExrAsync(memoryStream, new ExrEncoder()); + } + + memoryStream.Position = 0; + + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/x-exr", mime.DefaultMimeType); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index 5a8425c1fe..7686f77d95 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -85,11 +85,11 @@ public void DecodeThenEncodeImageFromStreamShouldSucceed() public static readonly TheoryData QuantizerNames = new() { - nameof(KnownQuantizers.Octree), - nameof(KnownQuantizers.WebSafe), - nameof(KnownQuantizers.Werner), - nameof(KnownQuantizers.Wu) - }; + nameof(KnownQuantizers.Octree), + nameof(KnownQuantizers.WebSafe), + nameof(KnownQuantizers.Werner), + nameof(KnownQuantizers.Wu) + }; [Theory] [WithFile(TestImages.Png.CalliphoraPartial, nameof(QuantizerNames), PixelTypes.Rgba32)] @@ -156,6 +156,11 @@ public void ImageCanConvertFormat() { image.SaveAsTiff(output); } + + using (FileStream output = File.OpenWrite(Path.Combine(path, $"{file.FileNameWithoutExtension}.exr"))) + { + image.SaveAsOpenExr(output); + } } } } @@ -210,6 +215,9 @@ public void ImageShouldPreservePixelByteOrderWhenSerialized() [InlineData(100, 100, "tiff")] [InlineData(100, 10, "tiff")] [InlineData(10, 100, "tiff")] + [InlineData(100, 100, "exr")] + [InlineData(100, 10, "exr")] + [InlineData(10, 100, "exr")] public void CanIdentifyImageLoadedFromBytes(int width, int height, string extension) { From 70cfcc9904b10d81c13d5d17c3e09ac3d0cc16f4 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 3 Jan 2022 12:47:40 +0100 Subject: [PATCH 09/66] 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 @@ internal static class AttributeNames public const string ScreenWindowWidth = "screenWindowWidth"; } + /// + /// EXR attribute types. + /// internal static class AttibuteTypes { public const string ChannelList = "chlist"; @@ -61,5 +64,16 @@ internal static class AttibuteTypes 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 @@ public Image Decode(BufferedReadStream stream, CancellationToken { 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 @@ public Image Decode(BufferedReadStream stream, CancellationToken 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 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf 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 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf break; - case "B": + case ExrConstants.ChannelNames.Blue: switch (channel.PixelType) { case ExrPixelType.Half: @@ -156,7 +158,7 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf break; - case "G": + case ExrConstants.ChannelNames.Green: switch (channel.PixelType) { case ExrPixelType.Half: @@ -169,6 +171,20 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf 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 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf 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 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf 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 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe 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 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe break; - case "B": + case ExrConstants.ChannelNames.Blue: switch (channel.PixelType) { case ExrPixelType.UnsignedInt: @@ -237,7 +267,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe break; - case "G": + case ExrConstants.ChannelNames.Green: switch (channel.PixelType) { case ExrPixelType.UnsignedInt: @@ -247,6 +277,17 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe 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 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe 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 @@ private ExrPixelType ValidateChannels() { 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 @@ public void Encode(Image image, Stream stream, CancellationToken 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 @@ public partial struct Rgb96 : IPixel 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})"); + } +} From 327976276814c82e9d22d8af95d243ed3f71899e Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 21 Jan 2022 18:25:58 +0100 Subject: [PATCH 10/66] Add support for decoding exr with zips compression --- .../Compressors/NoneExrCompression.cs | 24 ++++ .../Compressors/ZipsExrCompression.cs | 75 +++++++++++ .../OpenExr/Compression/ExrBaseCompression.cs | 43 +++++++ .../Compression/ExrBaseDecompressor.cs | 19 +++ .../ExrCompressionType.cs} | 4 +- .../Compression/ExrDecompressorFactory.cs | 24 ++++ .../Formats/OpenExr/ExrDecoderCore.cs | 121 ++++++++++++++---- .../Formats/OpenExr/ExrEncoderCore.cs | 5 +- src/ImageSharp/Formats/OpenExr/ExrHeader.cs | 3 +- .../Formats/OpenExr/ExrThrowHelper.cs | 3 + 10 files changed, 288 insertions(+), 33 deletions(-) create mode 100644 src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs create mode 100644 src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs create mode 100644 src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs create mode 100644 src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs rename src/ImageSharp/Formats/OpenExr/{ExrCompression.cs => Compression/ExrCompressionType.cs} (96%) create mode 100644 src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs new file mode 100644 index 0000000000..d4eaf67ebb --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors +{ + internal class NoneExrCompression : ExrBaseDecompressor + { + public NoneExrCompression(MemoryAllocator allocator, uint uncompressedBytes) + : base(allocator, uncompressedBytes) + { + } + + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + => stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.UncompressedBytes)); + + protected override void Dispose(bool disposing) + { + } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs new file mode 100644 index 0000000000..3f3db53279 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.IO.Compression; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors +{ + internal class ZipsExrCompression : ExrBaseDecompressor + { + private readonly IMemoryOwner tmpBuffer; + + public ZipsExrCompression(MemoryAllocator allocator, uint uncompressedBytes) + : base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate((int)uncompressedBytes); + + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + long pos = stream.Position; + using var deframeStream = new ZlibInflateStream( + stream, + () => + { + int left = (int)(compressedBytes - (stream.Position - pos)); + return left > 0 ? left : 0; + }); + deframeStream.AllocateNewBytes((int)this.UncompressedBytes, true); + DeflateStream dataStream = deframeStream.CompressedStream; + + Span tmp = this.tmpBuffer.GetSpan(); + int totalRead = 0; + while (totalRead < buffer.Length) + { + int bytesRead = dataStream.Read(tmp, totalRead, buffer.Length - totalRead); + if (bytesRead <= 0) + { + break; + } + + totalRead += bytesRead; + } + + Reconstruct(tmp, this.UncompressedBytes); + Interleave(tmp, this.UncompressedBytes, buffer); + } + + private static void Reconstruct(Span buffer, uint unCompressedBytes) + { + int offset = 0; + for (int i = 0; i < unCompressedBytes - 1; i++) + { + byte d = (byte)(buffer[offset] + (buffer[offset + 1] - 128)); + buffer[offset + 1] = d; + offset++; + } + } + + private static void Interleave(Span source, uint unCompressedBytes, Span output) + { + int sourceOffset = 0; + int offset0 = 0; + int offset1 = (int)((unCompressedBytes + 1) / 2); + while (sourceOffset < unCompressedBytes) + { + output[sourceOffset++] = source[offset0++]; + output[sourceOffset++] = source[offset1++]; + } + } + + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs new file mode 100644 index 0000000000..7faf4e77d6 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression +{ + internal abstract class ExrBaseCompression : IDisposable + { + private bool isDisposed; + + protected ExrBaseCompression(MemoryAllocator allocator, uint bytePerRow) + { + this.Allocator = allocator; + this.UncompressedBytes = bytePerRow; + } + + /// + /// Gets the memory allocator. + /// + protected MemoryAllocator Allocator { get; } + + /// + /// Gets the uncompressed bytes. + /// + public uint UncompressedBytes { get; } + + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; + this.Dispose(true); + } + + protected abstract void Dispose(bool disposing); + } +} diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs new file mode 100644 index 0000000000..1eb31ff528 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression +{ + internal abstract class ExrBaseDecompressor : ExrBaseCompression + { + protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytePerRow) + : base(allocator, bytePerRow) + { + } + + public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer); + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrCompressionType.cs similarity index 96% rename from src/ImageSharp/Formats/OpenExr/ExrCompression.cs rename to src/ImageSharp/Formats/OpenExr/Compression/ExrCompressionType.cs index 64df462ed3..3b47ed6ecb 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrCompressionType.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression { - internal enum ExrCompression + internal enum ExrCompressionType { /// /// Pixel data is not compressed. diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs new file mode 100644 index 0000000000..7ce46b4af5 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression +{ + internal static class ExrDecompressorFactory + { + public static ExrBaseDecompressor Create(ExrCompressionType method, MemoryAllocator memoryAllocator, uint bytesPerRow) + { + switch (method) + { + case ExrCompressionType.None: + return new NoneExrCompression(memoryAllocator, bytesPerRow); + case ExrCompressionType.Zips: + return new ZipsExrCompression(memoryAllocator, bytesPerRow); + default: + throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)); + } + } + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index 4a86dbad82..b5cae391df 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -5,8 +5,10 @@ using System.Buffers; using System.Buffers.Binary; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; +using SixLabors.ImageSharp.Formats.OpenExr.Compression; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -66,7 +68,7 @@ public ExrDecoderCore(Configuration configuration, IExrDecoderOptions options) private IList Channels { get; set; } - private ExrCompression Compression { get; set; } + private ExrCompressionType Compression { get; set; } /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) @@ -74,7 +76,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken { this.ReadExrHeader(stream); - if (this.Compression is not ExrCompression.None) + if (this.Compression is not ExrCompressionType.None and not ExrCompressionType.Zips) { ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); } @@ -104,12 +106,19 @@ public Image Decode(BufferedReadStream stream, CancellationToken private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buffer2D pixels) where TPixel : unmanaged, IPixel { + bool hasAlpha = this.HasAlpha(); + uint bytesPerRow = this.CalculateBytesPerRow(); + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 4); + using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerRow); + Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); 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); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerRow); + TPixel color = default; for (int y = 0; y < this.Height; y++) { @@ -124,9 +133,10 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); stream.Read(this.buffer, 0, 4); - uint pixelDataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + uint compressedBytesCount = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); - bool hasAlpha = false; + int offset = 0; for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { ExrChannelInfo channel = this.Channels[channelIdx]; @@ -136,10 +146,10 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf switch (channel.PixelType) { case ExrPixelType.Half: - this.ReadPixelRowChannelHalfSingle(stream, redPixelData); + offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), redPixelData); break; case ExrPixelType.Float: - this.ReadPixelRowChannelSingle(stream, redPixelData); + offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), redPixelData); break; } @@ -149,10 +159,10 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf switch (channel.PixelType) { case ExrPixelType.Half: - this.ReadPixelRowChannelHalfSingle(stream, bluePixelData); + offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), bluePixelData); break; case ExrPixelType.Float: - this.ReadPixelRowChannelSingle(stream, bluePixelData); + offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), bluePixelData); break; } @@ -162,24 +172,23 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf switch (channel.PixelType) { case ExrPixelType.Half: - this.ReadPixelRowChannelHalfSingle(stream, greenPixelData); + offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), greenPixelData); break; case ExrPixelType.Float: - this.ReadPixelRowChannelSingle(stream, greenPixelData); + offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), greenPixelData); break; } break; case ExrConstants.ChannelNames.Alpha: - hasAlpha = true; switch (channel.PixelType) { case ExrPixelType.Half: - this.ReadPixelRowChannelHalfSingle(stream, alphaPixelData); + offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), alphaPixelData); break; case ExrPixelType.Float: - this.ReadPixelRowChannelSingle(stream, alphaPixelData); + offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), alphaPixelData); break; } @@ -219,12 +228,19 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffer2D pixels) where TPixel : unmanaged, IPixel { + bool hasAlpha = this.HasAlpha(); + uint bytesPerRow = this.CalculateBytesPerRow(); + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 4); + using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerRow); + Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); 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); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerRow); + TPixel color = default; for (int y = 0; y < this.Height; y++) { @@ -239,9 +255,10 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); stream.Read(this.buffer, 0, 4); - uint pixelDataSize = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + uint compressedBytesCount = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); - bool hasAlpha = false; + int offset = 0; for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { ExrChannelInfo channel = this.Channels[channelIdx]; @@ -251,7 +268,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe switch (channel.PixelType) { case ExrPixelType.UnsignedInt: - this.ReadPixelRowChannelUnsignedInt(stream, redPixelData); + offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), redPixelData); break; } @@ -261,7 +278,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe switch (channel.PixelType) { case ExrPixelType.UnsignedInt: - this.ReadPixelRowChannelUnsignedInt(stream, bluePixelData); + offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), bluePixelData); break; } @@ -271,18 +288,17 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe switch (channel.PixelType) { case ExrPixelType.UnsignedInt: - this.ReadPixelRowChannelUnsignedInt(stream, greenPixelData); + offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), greenPixelData); break; } break; case ExrConstants.ChannelNames.Alpha: - hasAlpha = true; switch (channel.PixelType) { case ExrPixelType.UnsignedInt: - this.ReadPixelRowChannelUnsignedInt(stream, alphaPixelData); + offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), alphaPixelData); break; } @@ -319,29 +335,44 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe } } - private void ReadPixelRowChannelHalfSingle(BufferedReadStream stream, Span channelData) + private int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span channelData) { + int offset = 0; + ushort shortValue = 0; for (int x = 0; x < this.Width; x++) { - channelData[x] = stream.ReadHalfSingle(this.buffer); + shortValue = BinaryPrimitives.ReadUInt16LittleEndian(decompressedPixelData.Slice(offset, 2)); + channelData[x] = HalfTypeHelper.Unpack(shortValue); + offset += 2; } + + return offset; } - private void ReadPixelRowChannelSingle(BufferedReadStream stream, Span channelData) + private int ReadPixelRowChannelSingle(Span decompressedPixelData, Span channelData) { + int offset = 0; + int intValue = 0; for (int x = 0; x < this.Width; x++) { - channelData[x] = stream.ReadSingle(this.buffer); + intValue = BinaryPrimitives.ReadInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); + channelData[x] = Unsafe.As(ref intValue); + offset += 4; } + + return offset; } - private void ReadPixelRowChannelUnsignedInt(BufferedReadStream stream, Span channelData) + private int ReadPixelRowChannelUnsignedInt(Span decompressedPixelData, Span channelData) { + int offset = 0; for (int x = 0; x < this.Width; x++) { - stream.Read(this.buffer, 0, 4); - channelData[x] = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + channelData[x] = BinaryPrimitives.ReadUInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); + offset += 4; } + + return offset; } /// @@ -465,7 +496,7 @@ private ExrHeader ParseHeader(BufferedReadStream stream) header.Channels = channels; break; case ExrConstants.AttributeNames.Compression: - header.Compression = (ExrCompression)stream.ReadByte(); + header.Compression = (ExrCompressionType)stream.ReadByte(); break; case ExrConstants.AttributeNames.DataWindow: ExrBox2i dataWindow = this.ReadBoxInteger(stream); @@ -602,5 +633,39 @@ private static string ReadString(BufferedReadStream stream) return str.ToString(); } + + private bool HasAlpha() + { + foreach (ExrChannelInfo channelInfo in this.Channels) + { + if (channelInfo.ChannelName.Equals("A")) + { + return true; + } + } + + return false; + } + + private uint CalculateBytesPerRow() + { + uint bytesPerRow = 0; + foreach (ExrChannelInfo channelInfo in this.Channels) + { + if (channelInfo.ChannelName.Equals("A") || channelInfo.ChannelName.Equals("R") || channelInfo.ChannelName.Equals("G") || channelInfo.ChannelName.Equals("B")) + { + if (channelInfo.PixelType == ExrPixelType.Half) + { + bytesPerRow += 2 * (uint)this.Width; + } + else + { + bytesPerRow += 4 * (uint)this.Width; + } + } + } + + return bytesPerRow; + } } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs index 1890d0b2b1..63c171c354 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs @@ -8,6 +8,7 @@ using System.IO; using System.Runtime.CompilerServices; using System.Threading; +using SixLabors.ImageSharp.Formats.OpenExr.Compression; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -67,7 +68,7 @@ public void Encode(Image image, Stream stream, CancellationToken int height = image.Height; var header = new ExrHeader() { - Compression = ExrCompression.None, + Compression = ExrCompressionType.None, AspectRatio = 1.0f, DataWindow = new ExrBox2i(0, 0, width - 1, height - 1), DisplayWindow = new ExrBox2i(0, 0, width - 1, height - 1), @@ -293,7 +294,7 @@ private void WriteChannels(Stream stream, IList channels) stream.WriteByte(0); } - private void WriteCompression(Stream stream, ExrCompression compression) + private void WriteCompression(Stream stream, ExrCompressionType compression) { this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Compression, ExrConstants.AttibuteTypes.Compression, 1); stream.WriteByte((byte)compression); diff --git a/src/ImageSharp/Formats/OpenExr/ExrHeader.cs b/src/ImageSharp/Formats/OpenExr/ExrHeader.cs index 1ed464ebd2..a463abf72b 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrHeader.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrHeader.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.Collections.Generic; +using SixLabors.ImageSharp.Formats.OpenExr.Compression; namespace SixLabors.ImageSharp.Formats.OpenExr { @@ -9,7 +10,7 @@ internal class ExrHeader { public IList Channels { get; set; } - public ExrCompression? Compression { get; set; } + public ExrCompressionType? Compression { get; set; } public ExrBox2i? DataWindow { get; set; } diff --git a/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs b/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs index ae76541117..b68fb5a2f4 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(InliningOptions.ColdPath)] + public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}"); + [MethodImpl(MethodImplOptions.NoInlining)] public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); From 85501f3c007c48af54b54084a2245ab014ef94fc Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 21 Jan 2022 21:38:44 +0100 Subject: [PATCH 11/66] Add support for decoding RLE exr images --- .../Compressors/RunLengthCompression.cs | 94 +++++++++++++++++++ .../Compressors/ZipsExrCompression.cs | 32 +------ .../Compression/ExrBaseDecompressor.cs | 23 +++++ .../Compression/ExrDecompressorFactory.cs | 2 + .../Formats/OpenExr/ExrDecoderCore.cs | 3 +- 5 files changed, 126 insertions(+), 28 deletions(-) create mode 100644 src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs new file mode 100644 index 0000000000..de1bf52328 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors +{ + internal class RunLengthCompression : ExrBaseDecompressor + { + private readonly IMemoryOwner tmpBuffer; + + public RunLengthCompression(MemoryAllocator allocator, uint uncompressedBytes) + : base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate((int)uncompressedBytes); + + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + Span uncompressed = this.tmpBuffer.GetSpan(); + int maxLength = (int)this.UncompressedBytes; + int offset = 0; + while (compressedBytes > 0) + { + byte nextByte = ReadNextByte(stream); + + sbyte input = (sbyte)nextByte; + if (input < 0) + { + int count = -input; + compressedBytes -= (uint)(count + 1); + + if ((maxLength -= count) < 0) + { + return; + } + + // Check the input buffer is big enough to contain 'count' bytes of remaining data. + if (compressedBytes < 0) + { + return; + } + + for (int i = 0; i < count; i++) + { + uncompressed[offset + i] = ReadNextByte(stream); + } + + offset += count; + } + else + { + int count = input; + byte value = ReadNextByte(stream); + compressedBytes -= 2; + + if ((maxLength -= count + 1) < 0) + { + return; + } + + // Check the input buffer is big enough to contain byte to be duplicated. + if (compressedBytes < 0) + { + return; + } + + for (int i = 0; i < count + 1; i++) + { + uncompressed[offset + i] = value; + } + + offset += count + 1; + } + } + + Reconstruct(uncompressed, this.UncompressedBytes); + Interleave(uncompressed, this.UncompressedBytes, buffer); + } + + private static byte ReadNextByte(BufferedReadStream stream) + { + int nextByte = stream.ReadByte(); + if (nextByte == -1) + { + ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to decompress RLE image!"); + } + + return (byte)nextByte; + } + + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs index 3f3db53279..162893b11b 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs @@ -19,6 +19,8 @@ public ZipsExrCompression(MemoryAllocator allocator, uint uncompressedBytes) public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) { + Span uncompressed = this.tmpBuffer.GetSpan(); + long pos = stream.Position; using var deframeStream = new ZlibInflateStream( stream, @@ -30,11 +32,10 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, deframeStream.AllocateNewBytes((int)this.UncompressedBytes, true); DeflateStream dataStream = deframeStream.CompressedStream; - Span tmp = this.tmpBuffer.GetSpan(); int totalRead = 0; while (totalRead < buffer.Length) { - int bytesRead = dataStream.Read(tmp, totalRead, buffer.Length - totalRead); + int bytesRead = dataStream.Read(uncompressed, totalRead, buffer.Length - totalRead); if (bytesRead <= 0) { break; @@ -43,31 +44,8 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, totalRead += bytesRead; } - Reconstruct(tmp, this.UncompressedBytes); - Interleave(tmp, this.UncompressedBytes, buffer); - } - - private static void Reconstruct(Span buffer, uint unCompressedBytes) - { - int offset = 0; - for (int i = 0; i < unCompressedBytes - 1; i++) - { - byte d = (byte)(buffer[offset] + (buffer[offset + 1] - 128)); - buffer[offset + 1] = d; - offset++; - } - } - - private static void Interleave(Span source, uint unCompressedBytes, Span output) - { - int sourceOffset = 0; - int offset0 = 0; - int offset1 = (int)((unCompressedBytes + 1) / 2); - while (sourceOffset < unCompressedBytes) - { - output[sourceOffset++] = source[offset0++]; - output[sourceOffset++] = source[offset1++]; - } + Reconstruct(uncompressed, this.UncompressedBytes); + Interleave(uncompressed, this.UncompressedBytes, buffer); } protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs index 1eb31ff528..f8b811e502 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs @@ -15,5 +15,28 @@ protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytePerRow) } public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer); + + protected static void Reconstruct(Span buffer, uint unCompressedBytes) + { + int offset = 0; + for (int i = 0; i < unCompressedBytes - 1; i++) + { + byte d = (byte)(buffer[offset] + (buffer[offset + 1] - 128)); + buffer[offset + 1] = d; + offset++; + } + } + + protected static void Interleave(Span source, uint unCompressedBytes, Span output) + { + int sourceOffset = 0; + int offset0 = 0; + int offset1 = (int)((unCompressedBytes + 1) / 2); + while (sourceOffset < unCompressedBytes) + { + output[sourceOffset++] = source[offset0++]; + output[sourceOffset++] = source[offset1++]; + } + } } } diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs index 7ce46b4af5..ebbd3416fe 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs @@ -16,6 +16,8 @@ public static ExrBaseDecompressor Create(ExrCompressionType method, MemoryAlloca return new NoneExrCompression(memoryAllocator, bytesPerRow); case ExrCompressionType.Zips: return new ZipsExrCompression(memoryAllocator, bytesPerRow); + case ExrCompressionType.RunLengthEncoded: + return new RunLengthCompression(memoryAllocator, bytesPerRow); default: throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)); } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index b5cae391df..8674aa12c4 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading; using SixLabors.ImageSharp.Formats.OpenExr.Compression; +using SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -76,7 +77,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken { this.ReadExrHeader(stream); - if (this.Compression is not ExrCompressionType.None and not ExrCompressionType.Zips) + if (this.Compression is not ExrCompressionType.None and not ExrCompressionType.Zips and not ExrCompressionType.RunLengthEncoded) { ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); } From 69c8cd8a5f265e9cbcb64ca8e36689d516f73762 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 22 Jan 2022 19:11:56 +0100 Subject: [PATCH 12/66] Add support for ZIP compressed exr images --- ...ExrCompression.cs => ZipExrCompression.cs} | 8 +- .../Compression/ExrDecompressorFactory.cs | 10 +- .../Formats/OpenExr/ExrDecoderCore.cs | 336 ++++++++++-------- 3 files changed, 189 insertions(+), 165 deletions(-) rename src/ImageSharp/Formats/OpenExr/Compression/Compressors/{ZipsExrCompression.cs => ZipExrCompression.cs} (85%) diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs similarity index 85% rename from src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs rename to src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs index 162893b11b..508a5b4ac7 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipsExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs @@ -10,11 +10,11 @@ namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors { - internal class ZipsExrCompression : ExrBaseDecompressor + internal class ZipExrCompression : ExrBaseDecompressor { private readonly IMemoryOwner tmpBuffer; - public ZipsExrCompression(MemoryAllocator allocator, uint uncompressedBytes) + public ZipExrCompression(MemoryAllocator allocator, uint uncompressedBytes) : base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate((int)uncompressedBytes); public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) @@ -44,8 +44,8 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, totalRead += bytesRead; } - Reconstruct(uncompressed, this.UncompressedBytes); - Interleave(uncompressed, this.UncompressedBytes, buffer); + Reconstruct(uncompressed, (uint)totalRead); + Interleave(uncompressed, (uint)totalRead, buffer); } protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs index ebbd3416fe..8ba255d41c 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs @@ -8,16 +8,18 @@ namespace SixLabors.ImageSharp.Formats.OpenExr.Compression { internal static class ExrDecompressorFactory { - public static ExrBaseDecompressor Create(ExrCompressionType method, MemoryAllocator memoryAllocator, uint bytesPerRow) + public static ExrBaseDecompressor Create(ExrCompressionType method, MemoryAllocator memoryAllocator, uint uncompressedBytes) { switch (method) { case ExrCompressionType.None: - return new NoneExrCompression(memoryAllocator, bytesPerRow); + return new NoneExrCompression(memoryAllocator, uncompressedBytes); case ExrCompressionType.Zips: - return new ZipsExrCompression(memoryAllocator, bytesPerRow); + return new ZipExrCompression(memoryAllocator, uncompressedBytes); + case ExrCompressionType.Zip: + return new ZipExrCompression(memoryAllocator, uncompressedBytes); case ExrCompressionType.RunLengthEncoded: - return new RunLengthCompression(memoryAllocator, bytesPerRow); + return new RunLengthCompression(memoryAllocator, uncompressedBytes); default: throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)); } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index 8674aa12c4..aadbe425e5 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -9,7 +9,6 @@ using System.Text; using System.Threading; using SixLabors.ImageSharp.Formats.OpenExr.Compression; -using SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -77,7 +76,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken { this.ReadExrHeader(stream); - if (this.Compression is not ExrCompressionType.None and not ExrCompressionType.Zips and not ExrCompressionType.RunLengthEncoded) + if (this.Compression is not ExrCompressionType.None and not ExrCompressionType.Zips and not ExrCompressionType.Zip and not ExrCompressionType.RunLengthEncoded) { ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); } @@ -109,19 +108,21 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf { bool hasAlpha = this.HasAlpha(); uint bytesPerRow = this.CalculateBytesPerRow(); + uint rowsPerBlock = this.RowsPerBlock(); + uint bytesPerBlock = bytesPerRow * rowsPerBlock; using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 4); - using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerRow); + using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); 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); - using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerRow); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock); TPixel color = default; - for (int y = 0; y < this.Height; y++) + for (uint y = 0; y < this.Height; y += rowsPerBlock) { stream.Read(this.buffer, 0, 8); ulong rowOffset = BinaryPrimitives.ReadUInt64LittleEndian(this.buffer); @@ -129,100 +130,102 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf stream.Position = (long)rowOffset; stream.Read(this.buffer, 0, 4); - uint rowIndex = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); - - Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); + uint rowStartIndex = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); stream.Read(this.buffer, 0, 4); uint compressedBytesCount = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); int offset = 0; - for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) + for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < this.Height; rowIndex++) { - ExrChannelInfo channel = this.Channels[channelIdx]; - switch (channel.ChannelName) + Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); + for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { - case ExrConstants.ChannelNames.Red: - switch (channel.PixelType) - { - case ExrPixelType.Half: - offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), redPixelData); - break; - case ExrPixelType.Float: - offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), redPixelData); - break; - } - - break; - - case ExrConstants.ChannelNames.Blue: - switch (channel.PixelType) - { - case ExrPixelType.Half: - offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), bluePixelData); - break; - case ExrPixelType.Float: - offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), bluePixelData); - break; - } - - break; - - case ExrConstants.ChannelNames.Green: - switch (channel.PixelType) - { - case ExrPixelType.Half: - offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), greenPixelData); - break; - case ExrPixelType.Float: - offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), greenPixelData); - break; - } - - break; - - case ExrConstants.ChannelNames.Alpha: - switch (channel.PixelType) - { - case ExrPixelType.Half: - offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), alphaPixelData); - break; - case ExrPixelType.Float: - offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), alphaPixelData); - break; - } - - break; - - default: - // Skip unknown channel. - int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; - stream.Position += this.Width * channelDataSizeInBytes; - break; + ExrChannelInfo channel = this.Channels[channelIdx]; + switch (channel.ChannelName) + { + case ExrConstants.ChannelNames.Red: + switch (channel.PixelType) + { + case ExrPixelType.Half: + offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), redPixelData); + break; + case ExrPixelType.Float: + offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), redPixelData); + break; + } + + break; + + case ExrConstants.ChannelNames.Blue: + switch (channel.PixelType) + { + case ExrPixelType.Half: + offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), bluePixelData); + break; + case ExrPixelType.Float: + offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), bluePixelData); + break; + } + + break; + + case ExrConstants.ChannelNames.Green: + switch (channel.PixelType) + { + case ExrPixelType.Half: + offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), greenPixelData); + break; + case ExrPixelType.Float: + offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), greenPixelData); + break; + } + + break; + + case ExrConstants.ChannelNames.Alpha: + switch (channel.PixelType) + { + case ExrPixelType.Half: + offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), alphaPixelData); + break; + case ExrPixelType.Float: + offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), alphaPixelData); + 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; - if (hasAlpha) - { - for (int x = 0; x < this.Width; x++) + if (hasAlpha) { - var pixelValue = new HalfVector4(redPixelData[x], greenPixelData[x], bluePixelData[x], alphaPixelData[x]); - 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], alphaPixelData[x]); + color.FromVector4(pixelValue.ToVector4()); + pixelRow[x] = color; + } } - } - else - { - for (int x = 0; x < this.Width; x++) + 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; + } } } + + stream.Position = nextRowOffsetPosition; } } @@ -231,19 +234,21 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe { bool hasAlpha = this.HasAlpha(); uint bytesPerRow = this.CalculateBytesPerRow(); + uint rowsPerBlock = this.RowsPerBlock(); + uint bytesPerBlock = bytesPerRow * rowsPerBlock; using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 4); - using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerRow); + using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); 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); - using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerRow); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock); TPixel color = default; - for (int y = 0; y < this.Height; y++) + for (uint y = 0; y < this.Height; y += rowsPerBlock) { stream.Read(this.buffer, 0, 8); ulong rowOffset = BinaryPrimitives.ReadUInt64LittleEndian(this.buffer); @@ -251,86 +256,88 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe stream.Position = (long)rowOffset; stream.Read(this.buffer, 0, 4); - uint rowIndex = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); - - Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); + uint rowStartIndex = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); stream.Read(this.buffer, 0, 4); uint compressedBytesCount = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); int offset = 0; - for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) + for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < this.Height; rowIndex++) { - ExrChannelInfo channel = this.Channels[channelIdx]; - switch (channel.ChannelName) + Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); + for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { - case ExrConstants.ChannelNames.Red: - switch (channel.PixelType) - { - case ExrPixelType.UnsignedInt: - offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), redPixelData); - break; - } - - break; - - case ExrConstants.ChannelNames.Blue: - switch (channel.PixelType) - { - case ExrPixelType.UnsignedInt: - offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), bluePixelData); - break; - } - - break; - - case ExrConstants.ChannelNames.Green: - switch (channel.PixelType) - { - case ExrPixelType.UnsignedInt: - offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), greenPixelData); - break; - } - - break; - - case ExrConstants.ChannelNames.Alpha: - switch (channel.PixelType) - { - case ExrPixelType.UnsignedInt: - offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), alphaPixelData); - break; - } - - break; - - default: - // Skip unknown channel. - int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; - stream.Position += this.Width * channelDataSizeInBytes; - break; + ExrChannelInfo channel = this.Channels[channelIdx]; + switch (channel.ChannelName) + { + case ExrConstants.ChannelNames.Red: + switch (channel.PixelType) + { + case ExrPixelType.UnsignedInt: + offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), redPixelData); + break; + } + + break; + + case ExrConstants.ChannelNames.Blue: + switch (channel.PixelType) + { + case ExrPixelType.UnsignedInt: + offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), bluePixelData); + break; + } + + break; + + case ExrConstants.ChannelNames.Green: + switch (channel.PixelType) + { + case ExrPixelType.UnsignedInt: + offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), greenPixelData); + break; + } + + break; + + case ExrConstants.ChannelNames.Alpha: + switch (channel.PixelType) + { + case ExrPixelType.UnsignedInt: + offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), alphaPixelData); + 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; + stream.Position = nextRowOffsetPosition; - if (hasAlpha) - { - for (int x = 0; x < this.Width; x++) + if (hasAlpha) { - var pixelValue = new Rgba128(redPixelData[x], greenPixelData[x], bluePixelData[x], alphaPixelData[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++) + else { - 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 Rgb96(redPixelData[x], greenPixelData[x], bluePixelData[x]); + color.FromVector4(pixelValue.ToVector4()); + pixelRow[x] = color; + } } } } @@ -339,10 +346,9 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe private int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span channelData) { int offset = 0; - ushort shortValue = 0; for (int x = 0; x < this.Width; x++) { - shortValue = BinaryPrimitives.ReadUInt16LittleEndian(decompressedPixelData.Slice(offset, 2)); + ushort shortValue = BinaryPrimitives.ReadUInt16LittleEndian(decompressedPixelData.Slice(offset, 2)); channelData[x] = HalfTypeHelper.Unpack(shortValue); offset += 2; } @@ -353,10 +359,9 @@ private int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span private int ReadPixelRowChannelSingle(Span decompressedPixelData, Span channelData) { int offset = 0; - int intValue = 0; for (int x = 0; x < this.Width; x++) { - intValue = BinaryPrimitives.ReadInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); + int intValue = BinaryPrimitives.ReadInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); channelData[x] = Unsafe.As(ref intValue); offset += 4; } @@ -668,5 +673,22 @@ private uint CalculateBytesPerRow() return bytesPerRow; } + + private uint RowsPerBlock() + { + switch (this.Compression) + { + case ExrCompressionType.Zip: + case ExrCompressionType.Pxr24: + return 16; + case ExrCompressionType.B44: + case ExrCompressionType.B44A: + case ExrCompressionType.Piz: + return 32; + + default: + return 1; + } + } } } From fdc7b61b4b2a6d73fd8a7032ba10c88425a23c64 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 23 Jan 2022 19:43:38 +0100 Subject: [PATCH 13/66] Add support for decoding gray exr images --- .../Compressors/ZipExrCompression.cs | 5 + .../Formats/OpenExr/ExrConstants.cs | 2 + .../Formats/OpenExr/ExrDecoderCore.cs | 161 +++++++++++++----- .../Formats/OpenExr/ExrImageType.cs | 16 ++ 4 files changed, 143 insertions(+), 41 deletions(-) create mode 100644 src/ImageSharp/Formats/OpenExr/ExrImageType.cs diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs index 508a5b4ac7..65a2ea3485 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs @@ -44,6 +44,11 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, totalRead += bytesRead; } + if (totalRead == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read zip compressed image data!"); + } + Reconstruct(uncompressed, (uint)totalRead); Interleave(uncompressed, (uint)totalRead, buffer); } diff --git a/src/ImageSharp/Formats/OpenExr/ExrConstants.cs b/src/ImageSharp/Formats/OpenExr/ExrConstants.cs index 079adec7ed..ad6b1763d9 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrConstants.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrConstants.cs @@ -74,6 +74,8 @@ internal static class ChannelNames public const string Blue = "B"; public const string Alpha = "A"; + + public const string Luminance = "Y"; } } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index aadbe425e5..a41ceb0766 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -70,18 +70,16 @@ public ExrDecoderCore(Configuration configuration, IExrDecoderOptions options) private ExrCompressionType Compression { get; set; } + private ExrImageType ImageType { get; set; } + /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { this.ReadExrHeader(stream); - - if (this.Compression is not ExrCompressionType.None and not ExrCompressionType.Zips and not ExrCompressionType.Zip and not ExrCompressionType.RunLengthEncoded) - { - ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); - } - + this.IsSupportedCompression(); ExrPixelType pixelType = this.ValidateChannels(); + this.ReadImageType(); var image = new Image(this.Configuration, this.Width, this.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); @@ -197,6 +195,22 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf break; + case ExrConstants.ChannelNames.Luminance: + switch (channel.PixelType) + { + case ExrPixelType.Half: + offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), redPixelData); + break; + case ExrPixelType.Float: + offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), redPixelData); + break; + } + + redPixelData.CopyTo(bluePixelData); + redPixelData.CopyTo(greenPixelData); + + break; + default: // Skip unknown channel. int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; @@ -418,41 +432,9 @@ private ExrPixelType ValidateChannels() } // 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; - } - } - - if (!pixelType.HasValue) - { - ExrThrowHelper.ThrowInvalidImageContentException("Pixel channel data is unknown! Only R, G, B and A are supported."); - } + ExrPixelType pixelType = this.FindPixelType(); - 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.Value; + return pixelType; } private ExrHeader ReadExrHeader(BufferedReadStream stream) @@ -640,6 +622,103 @@ private static string ReadString(BufferedReadStream stream) return str.ToString(); } + private ExrPixelType FindPixelType() + { + 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) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Luminance)) + { + if (!pixelType.HasValue) + { + pixelType = this.Channels[i].PixelType; + } + else + { + if (pixelType != this.Channels[i].PixelType) + { + ExrThrowHelper.ThrowNotSupported("Pixel channel data is expected to be the same for all channels."); + } + } + } + } + + if (!pixelType.HasValue) + { + ExrThrowHelper.ThrowNotSupported("Pixel channel data is unknown! Only R, G, B, A and Y are supported."); + } + + return pixelType.Value; + } + + private void IsSupportedCompression() + { + if (this.Compression is not ExrCompressionType.None and not ExrCompressionType.Zips and not ExrCompressionType.Zip and not ExrCompressionType.RunLengthEncoded) + { + ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); + } + } + + private void ReadImageType() + { + bool hasRedChannel = false; + bool hasGreenChannel = false; + bool hasBlueChannel = false; + bool hasAlphaChannel = false; + bool hasLuminance = false; + foreach (ExrChannelInfo channelInfo in this.Channels) + { + if (channelInfo.ChannelName.Equals("A")) + { + hasAlphaChannel = true; + } + + if (channelInfo.ChannelName.Equals("R")) + { + hasRedChannel = true; + } + + if (channelInfo.ChannelName.Equals("G")) + { + hasGreenChannel = true; + } + + if (channelInfo.ChannelName.Equals("B")) + { + hasBlueChannel = true; + } + + if (channelInfo.ChannelName.Equals("Y")) + { + hasLuminance = true; + } + } + + if (hasRedChannel && hasGreenChannel && hasBlueChannel && hasAlphaChannel) + { + this.ImageType = ExrImageType.Rgba; + return; + } + + if (hasRedChannel && hasGreenChannel && hasBlueChannel) + { + this.ImageType = ExrImageType.Rgb; + return; + } + + if (hasLuminance && this.Channels.Count == 1) + { + this.ImageType = ExrImageType.Gray; + return; + } + + ExrThrowHelper.ThrowNotSupported("The image contains channels, which are not supported!"); + } + private bool HasAlpha() { foreach (ExrChannelInfo channelInfo in this.Channels) @@ -658,7 +737,7 @@ private uint CalculateBytesPerRow() uint bytesPerRow = 0; foreach (ExrChannelInfo channelInfo in this.Channels) { - if (channelInfo.ChannelName.Equals("A") || channelInfo.ChannelName.Equals("R") || channelInfo.ChannelName.Equals("G") || channelInfo.ChannelName.Equals("B")) + if (channelInfo.ChannelName.Equals("A") || channelInfo.ChannelName.Equals("R") || channelInfo.ChannelName.Equals("G") || channelInfo.ChannelName.Equals("B") || channelInfo.ChannelName.Equals("Y")) { if (channelInfo.PixelType == ExrPixelType.Half) { diff --git a/src/ImageSharp/Formats/OpenExr/ExrImageType.cs b/src/ImageSharp/Formats/OpenExr/ExrImageType.cs new file mode 100644 index 0000000000..e4e9c8a649 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrImageType.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + internal enum ExrImageType + { + Unknown = 0, + + Rgb = 1, + + Rgba = 2, + + Gray = 3, + } +} From fd0c245439947ea0783209b4925b5a9a35eefaa6 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 27 Jan 2022 15:17:31 +0100 Subject: [PATCH 14/66] Refactor --- .../Formats/OpenExr/ExrConstants.cs | 4 + .../Formats/OpenExr/ExrDecoderCore.cs | 456 +++++++++--------- .../Formats/OpenExr/ExrEncoderCore.cs | 4 +- .../{ExrHeader.cs => ExrHeaderAttributes.cs} | 8 +- .../Formats/OpenExr/ExrImageDataType.cs | 16 + .../Formats/OpenExr/ExrImageType.cs | 8 +- 6 files changed, 255 insertions(+), 241 deletions(-) rename src/ImageSharp/Formats/OpenExr/{ExrHeader.cs => ExrHeaderAttributes.cs} (89%) create mode 100644 src/ImageSharp/Formats/OpenExr/ExrImageDataType.cs diff --git a/src/ImageSharp/Formats/OpenExr/ExrConstants.cs b/src/ImageSharp/Formats/OpenExr/ExrConstants.cs index ad6b1763d9..29adb7b887 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrConstants.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrConstants.cs @@ -45,6 +45,10 @@ internal static class AttributeNames public const string ScreenWindowCenter = "screenWindowCenter"; public const string ScreenWindowWidth = "screenWindowWidth"; + + public const string Tiles = "tiles"; + + public const string ChunkCount = "chunkCount"; } /// diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index a41ceb0766..d414babf28 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -70,8 +70,12 @@ public ExrDecoderCore(Configuration configuration, IExrDecoderOptions options) private ExrCompressionType Compression { get; set; } + private ExrImageDataType ImageDataType { get; set; } + private ExrImageType ImageType { get; set; } + private ExrHeaderAttributes HeaderAttributes { get; set; } + /// public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel @@ -79,7 +83,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken this.ReadExrHeader(stream); this.IsSupportedCompression(); ExrPixelType pixelType = this.ValidateChannels(); - this.ReadImageType(); + this.ReadImageDataType(); var image = new Image(this.Configuration, this.Width, this.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); @@ -105,138 +109,51 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf where TPixel : unmanaged, IPixel { bool hasAlpha = this.HasAlpha(); - uint bytesPerRow = this.CalculateBytesPerRow(); + uint bytesPerRow = this.CalculateBytesPerRow((uint)this.Width); uint rowsPerBlock = this.RowsPerBlock(); uint bytesPerBlock = bytesPerRow * rowsPerBlock; + int width = this.Width; + int height = this.Height; - using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 4); + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); - 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); + Span redPixelData = rowBuffer.GetSpan().Slice(0, width); + Span greenPixelData = rowBuffer.GetSpan().Slice(width, width); + Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); + Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock); TPixel color = default; - for (uint y = 0; y < this.Height; y += rowsPerBlock) + for (uint y = 0; y < height; y += rowsPerBlock) { - stream.Read(this.buffer, 0, 8); - ulong rowOffset = BinaryPrimitives.ReadUInt64LittleEndian(this.buffer); + ulong rowOffset = this.ReadUnsignedLong(stream); long nextRowOffsetPosition = stream.Position; stream.Position = (long)rowOffset; - stream.Read(this.buffer, 0, 4); - uint rowStartIndex = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + uint rowStartIndex = this.ReadUnsignedInteger(stream); - stream.Read(this.buffer, 0, 4); - uint compressedBytesCount = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + uint compressedBytesCount = this.ReadUnsignedInteger(stream); decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); int offset = 0; - for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < this.Height; rowIndex++) + for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) { Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { ExrChannelInfo channel = this.Channels[channelIdx]; - switch (channel.ChannelName) - { - case ExrConstants.ChannelNames.Red: - switch (channel.PixelType) - { - case ExrPixelType.Half: - offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), redPixelData); - break; - case ExrPixelType.Float: - offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), redPixelData); - break; - } - - break; - - case ExrConstants.ChannelNames.Blue: - switch (channel.PixelType) - { - case ExrPixelType.Half: - offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), bluePixelData); - break; - case ExrPixelType.Float: - offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), bluePixelData); - break; - } - - break; - - case ExrConstants.ChannelNames.Green: - switch (channel.PixelType) - { - case ExrPixelType.Half: - offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), greenPixelData); - break; - case ExrPixelType.Float: - offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), greenPixelData); - break; - } - - break; - - case ExrConstants.ChannelNames.Alpha: - switch (channel.PixelType) - { - case ExrPixelType.Half: - offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), alphaPixelData); - break; - case ExrPixelType.Float: - offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), alphaPixelData); - break; - } - - break; - - case ExrConstants.ChannelNames.Luminance: - switch (channel.PixelType) - { - case ExrPixelType.Half: - offset += this.ReadPixelRowChannelHalfSingle(decompressedPixelData.Slice(offset), redPixelData); - break; - case ExrPixelType.Float: - offset += this.ReadPixelRowChannelSingle(decompressedPixelData.Slice(offset), redPixelData); - break; - } - - redPixelData.CopyTo(bluePixelData); - redPixelData.CopyTo(greenPixelData); - - break; - - default: - // Skip unknown channel. - int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; - stream.Position += this.Width * channelDataSizeInBytes; - break; - } + offset += this.ReadFloatChannelData(stream, channel, decompressedPixelData.Slice(offset), redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); } - if (hasAlpha) + for (int x = 0; x < width; x++) { - 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 - { - 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; - } + var pixelValue = new HalfVector4(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1.0f); + color.FromVector4(pixelValue.ToVector4()); + pixelRow[x] = color; } + } stream.Position = nextRowOffsetPosition; @@ -247,120 +164,161 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe where TPixel : unmanaged, IPixel { bool hasAlpha = this.HasAlpha(); - uint bytesPerRow = this.CalculateBytesPerRow(); + uint bytesPerRow = this.CalculateBytesPerRow((uint)this.Width); uint rowsPerBlock = this.RowsPerBlock(); uint bytesPerBlock = bytesPerRow * rowsPerBlock; + int width = this.Width; + int height = this.Height; - using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(this.Width * 4); + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); - 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); + Span redPixelData = rowBuffer.GetSpan().Slice(0, width); + Span greenPixelData = rowBuffer.GetSpan().Slice(width, width); + Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); + Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock); TPixel color = default; - for (uint y = 0; y < this.Height; y += rowsPerBlock) + for (uint y = 0; y < height; y += rowsPerBlock) { - stream.Read(this.buffer, 0, 8); - ulong rowOffset = BinaryPrimitives.ReadUInt64LittleEndian(this.buffer); + ulong rowOffset = this.ReadUnsignedLong(stream); long nextRowOffsetPosition = stream.Position; stream.Position = (long)rowOffset; - stream.Read(this.buffer, 0, 4); - uint rowStartIndex = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + uint rowStartIndex = this.ReadUnsignedInteger(stream); - stream.Read(this.buffer, 0, 4); - uint compressedBytesCount = BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + uint compressedBytesCount = this.ReadUnsignedInteger(stream); decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); int offset = 0; - for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < this.Height; rowIndex++) + for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) { Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { ExrChannelInfo channel = this.Channels[channelIdx]; - switch (channel.ChannelName) - { - case ExrConstants.ChannelNames.Red: - switch (channel.PixelType) - { - case ExrPixelType.UnsignedInt: - offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), redPixelData); - break; - } - - break; - - case ExrConstants.ChannelNames.Blue: - switch (channel.PixelType) - { - case ExrPixelType.UnsignedInt: - offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), bluePixelData); - break; - } - - break; - - case ExrConstants.ChannelNames.Green: - switch (channel.PixelType) - { - case ExrPixelType.UnsignedInt: - offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), greenPixelData); - break; - } - - break; - - case ExrConstants.ChannelNames.Alpha: - switch (channel.PixelType) - { - case ExrPixelType.UnsignedInt: - offset += this.ReadPixelRowChannelUnsignedInt(decompressedPixelData.Slice(offset), alphaPixelData); - break; - } - - break; - - default: - // Skip unknown channel. - int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; - stream.Position += this.Width * channelDataSizeInBytes; - break; - } + offset += this.ReadUnsignedIntChannelData(stream, channel, decompressedPixelData, redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); } stream.Position = nextRowOffsetPosition; - if (hasAlpha) + for (int x = 0; x < width; x++) { - 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; - } + var pixelValue = new Rgba128(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue); + color.FromVector4(pixelValue.ToVector4()); + pixelRow[x] = color; } } } } - private int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span channelData) + private int ReadFloatChannelData( + BufferedReadStream stream, + ExrChannelInfo channel, + Span decompressedPixelData, + Span redPixelData, + Span greenPixelData, + Span bluePixelData, + Span alphaPixelData, + int width) + { + switch (channel.ChannelName) + { + case ExrConstants.ChannelNames.Red: + return this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); + + case ExrConstants.ChannelNames.Blue: + return this.ReadChannelData(channel, decompressedPixelData, bluePixelData, width); + + case ExrConstants.ChannelNames.Green: + return this.ReadChannelData(channel, decompressedPixelData, greenPixelData, width); + + case ExrConstants.ChannelNames.Alpha: + return this.ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); + + case ExrConstants.ChannelNames.Luminance: + int bytesRead = this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); + redPixelData.CopyTo(bluePixelData); + redPixelData.CopyTo(greenPixelData); + + return bytesRead; + + default: + // Skip unknown channel. + int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; + stream.Position += width * channelDataSizeInBytes; + return channelDataSizeInBytes; + } + } + + private int ReadUnsignedIntChannelData( + BufferedReadStream stream, + ExrChannelInfo channel, + Span decompressedPixelData, + Span redPixelData, + Span greenPixelData, + Span bluePixelData, + Span alphaPixelData, + int width) + { + switch (channel.ChannelName) + { + case ExrConstants.ChannelNames.Red: + return this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); + + case ExrConstants.ChannelNames.Blue: + return this.ReadChannelData(channel, decompressedPixelData, bluePixelData, width); + + case ExrConstants.ChannelNames.Green: + return this.ReadChannelData(channel, decompressedPixelData, greenPixelData, width); + + case ExrConstants.ChannelNames.Alpha: + return this.ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); + + case ExrConstants.ChannelNames.Luminance: + int bytesRead = this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); + redPixelData.CopyTo(bluePixelData); + redPixelData.CopyTo(greenPixelData); + return bytesRead; + + default: + // Skip unknown channel. + int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; + stream.Position += this.Width * channelDataSizeInBytes; + return channelDataSizeInBytes; + } + } + + private int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) + { + switch (channel.PixelType) + { + case ExrPixelType.Half: + return this.ReadPixelRowChannelHalfSingle(decompressedPixelData, pixelData, width); + case ExrPixelType.Float: + return this.ReadPixelRowChannelSingle(decompressedPixelData, pixelData, width); + } + + return 0; + } + + private int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) + { + switch (channel.PixelType) + { + case ExrPixelType.UnsignedInt: + return this.ReadPixelRowChannelUnsignedInt(decompressedPixelData, pixelData, width); + } + + return 0; + } + + private int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span channelData, int width) { int offset = 0; - for (int x = 0; x < this.Width; x++) + for (int x = 0; x < width; x++) { ushort shortValue = BinaryPrimitives.ReadUInt16LittleEndian(decompressedPixelData.Slice(offset, 2)); channelData[x] = HalfTypeHelper.Unpack(shortValue); @@ -370,10 +328,10 @@ private int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span return offset; } - private int ReadPixelRowChannelSingle(Span decompressedPixelData, Span channelData) + private int ReadPixelRowChannelSingle(Span decompressedPixelData, Span channelData, int width) { int offset = 0; - for (int x = 0; x < this.Width; x++) + for (int x = 0; x < width; x++) { int intValue = BinaryPrimitives.ReadInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); channelData[x] = Unsafe.As(ref intValue); @@ -383,10 +341,10 @@ private int ReadPixelRowChannelSingle(Span decompressedPixelData, Span decompressedPixelData, Span channelData) + private int ReadPixelRowChannelUnsignedInt(Span decompressedPixelData, Span channelData, int width) { int offset = 0; - for (int x = 0; x < this.Width; x++) + for (int x = 0; x < width; x++) { channelData[x] = BinaryPrimitives.ReadUInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); offset += 4; @@ -398,7 +356,7 @@ private int ReadPixelRowChannelUnsignedInt(Span decompressedPixelData, Spa /// public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { - ExrHeader header = this.ReadExrHeader(stream); + ExrHeaderAttributes header = this.ReadExrHeader(stream); int bitsPerPixel = this.CalculateBitsPerPixel(); @@ -437,10 +395,10 @@ private ExrPixelType ValidateChannels() return pixelType; } - private ExrHeader ReadExrHeader(BufferedReadStream stream) + private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) { - // Skip over the magick bytes. - stream.Read(this.buffer, 0, 4); + // Skip over the magick bytes, we already know its an EXR image. + stream.Skip(4); // Read version number. byte version = (byte)stream.ReadByte(); @@ -450,30 +408,35 @@ private ExrHeader ReadExrHeader(BufferedReadStream stream) } // Next three bytes contain info's about the image. - // TODO: We ignore those for now. - stream.Read(this.buffer, 0, 3); + byte flagsByte0 = (byte)stream.ReadByte(); + byte flagsByte1 = (byte)stream.ReadByte(); + byte flagsByte2 = (byte)stream.ReadByte(); + if ((flagsByte0 & (1 << 1)) != 0) + { + this.ImageType = ExrImageType.Tiled; + } - ExrHeader header = this.ParseHeader(stream); + this.HeaderAttributes = this.ParseHeaderAttributes(stream); - if (!header.IsValid()) + if (!this.HeaderAttributes.IsValid()) { 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.Channels = header.Channels; - this.Compression = header.Compression.GetValueOrDefault(); + this.Width = this.HeaderAttributes.DataWindow.Value.XMax - this.HeaderAttributes.DataWindow.Value.XMin + 1; + this.Height = this.HeaderAttributes.DataWindow.Value.YMax - this.HeaderAttributes.DataWindow.Value.YMin + 1; + this.Channels = this.HeaderAttributes.Channels; + this.Compression = this.HeaderAttributes.Compression.GetValueOrDefault(); this.metadata = new ImageMetadata(); - return header; + return this.HeaderAttributes; } - private ExrHeader ParseHeader(BufferedReadStream stream) + private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) { ExrAttribute attribute = this.ReadAttribute(stream); - var header = new ExrHeader(); + var header = new ExrHeaderAttributes(); while (!attribute.Equals(ExrAttribute.EmptyAttribute)) { @@ -511,6 +474,13 @@ private ExrHeader ParseHeader(BufferedReadStream stream) float screenWindowWidth = stream.ReadSingle(this.buffer); header.ScreenWindowWidth = screenWindowWidth; break; + case ExrConstants.AttributeNames.Tiles: + header.TileXSize = this.ReadUnsignedInteger(stream); + header.TileYSize = this.ReadUnsignedInteger(stream); + break; + case ExrConstants.AttributeNames.ChunkCount: + header.ChunkCount = this.ReadSignedInteger(stream); + break; default: // Skip unknown attribute bytes. stream.Skip(attribute.Length); @@ -533,25 +503,17 @@ private ExrAttribute ReadAttribute(BufferedReadStream stream) string attributeType = ReadString(stream); - stream.Read(this.buffer, 0, 4); - int attributeSize = BinaryPrimitives.ReadInt32LittleEndian(this.buffer); + int attributeSize = this.ReadSignedInteger(stream); return new ExrAttribute(attributeName, attributeType, attributeSize); } private ExrBox2i ReadBoxInteger(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); + int xMin = this.ReadSignedInteger(stream); + int yMin = this.ReadSignedInteger(stream); + int xMax = this.ReadSignedInteger(stream); + int yMax = this.ReadSignedInteger(stream); return new ExrBox2i(xMin, yMin, xMax, yMax); } @@ -577,9 +539,8 @@ private ExrChannelInfo ReadChannelInfo(BufferedReadStream stream, out int bytesR string channelName = ReadString(stream); bytesRead = channelName.Length + 1; - stream.Read(this.buffer, 0, 4); + var pixelType = (ExrPixelType)this.ReadSignedInteger(stream); bytesRead += 4; - var pixelType = (ExrPixelType)BinaryPrimitives.ReadInt32LittleEndian(this.buffer); byte pLinear = (byte)stream.ReadByte(); byte reserved0 = (byte)stream.ReadByte(); @@ -587,13 +548,11 @@ private ExrChannelInfo ReadChannelInfo(BufferedReadStream stream, out int bytesR byte reserved2 = (byte)stream.ReadByte(); bytesRead += 4; - stream.Read(this.buffer, 0, 4); + int xSampling = this.ReadSignedInteger(stream); bytesRead += 4; - int xSampling = BinaryPrimitives.ReadInt32LittleEndian(this.buffer); - stream.Read(this.buffer, 0, 4); + int ySampling = this.ReadSignedInteger(stream); bytesRead += 4; - int ySampling = BinaryPrimitives.ReadInt32LittleEndian(this.buffer); return new ExrChannelInfo(channelName, pixelType, pLinear, xSampling, ySampling); } @@ -663,7 +622,7 @@ private void IsSupportedCompression() } } - private void ReadImageType() + private void ReadImageDataType() { bool hasRedChannel = false; bool hasGreenChannel = false; @@ -700,19 +659,19 @@ private void ReadImageType() if (hasRedChannel && hasGreenChannel && hasBlueChannel && hasAlphaChannel) { - this.ImageType = ExrImageType.Rgba; + this.ImageDataType = ExrImageDataType.Rgba; return; } if (hasRedChannel && hasGreenChannel && hasBlueChannel) { - this.ImageType = ExrImageType.Rgb; + this.ImageDataType = ExrImageDataType.Rgb; return; } if (hasLuminance && this.Channels.Count == 1) { - this.ImageType = ExrImageType.Gray; + this.ImageDataType = ExrImageDataType.Gray; return; } @@ -732,7 +691,7 @@ private bool HasAlpha() return false; } - private uint CalculateBytesPerRow() + private uint CalculateBytesPerRow(uint width) { uint bytesPerRow = 0; foreach (ExrChannelInfo channelInfo in this.Channels) @@ -741,11 +700,11 @@ private uint CalculateBytesPerRow() { if (channelInfo.PixelType == ExrPixelType.Half) { - bytesPerRow += 2 * (uint)this.Width; + bytesPerRow += 2 * width; } else { - bytesPerRow += 4 * (uint)this.Width; + bytesPerRow += 4 * width; } } } @@ -769,5 +728,38 @@ private uint RowsPerBlock() return 1; } } + + private ulong ReadUnsignedLong(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 8); + if (bytesRead != 8) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); + } + + return BinaryPrimitives.ReadUInt64LittleEndian(this.buffer); + } + + private uint ReadUnsignedInteger(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 4); + if (bytesRead != 4) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); + } + + return BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + } + + private int ReadSignedInteger(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 4); + if (bytesRead != 4) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); + } + + return BinaryPrimitives.ReadInt32LittleEndian(this.buffer); + } } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs index 63c171c354..eb626dc1b2 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs @@ -66,7 +66,7 @@ public void Encode(Image image, Stream stream, CancellationToken this.pixelType ??= exrMetadata.PixelType; int width = image.Width; int height = image.Height; - var header = new ExrHeader() + var header = new ExrHeaderAttributes() { Compression = ExrCompressionType.None, AspectRatio = 1.0f, @@ -193,7 +193,7 @@ private void EncodeUnsignedIntPixelData(Stream stream, Buffer2D } } - private void WriteHeader(Stream stream, ExrHeader header) + private void WriteHeader(Stream stream, ExrHeaderAttributes header) { this.WriteChannels(stream, header.Channels); this.WriteCompression(stream, header.Compression.Value); diff --git a/src/ImageSharp/Formats/OpenExr/ExrHeader.cs b/src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs similarity index 89% rename from src/ImageSharp/Formats/OpenExr/ExrHeader.cs rename to src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs index a463abf72b..76d14311a5 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrHeader.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr { - internal class ExrHeader + internal class ExrHeaderAttributes { public IList Channels { get; set; } @@ -24,6 +24,12 @@ internal class ExrHeader public PointF? ScreenWindowCenter { get; set; } + public uint? TileXSize { get; set; } + + public uint? TileYSize { get; set; } + + public int? ChunkCount { get; set; } + public bool IsValid() { if (!this.Compression.HasValue) diff --git a/src/ImageSharp/Formats/OpenExr/ExrImageDataType.cs b/src/ImageSharp/Formats/OpenExr/ExrImageDataType.cs new file mode 100644 index 0000000000..d17c3ad61d --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrImageDataType.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Formats.OpenExr +{ + internal enum ExrImageDataType + { + Unknown = 0, + + Rgb = 1, + + Rgba = 2, + + Gray = 3, + } +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrImageType.cs b/src/ImageSharp/Formats/OpenExr/ExrImageType.cs index e4e9c8a649..e18ee363f2 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrImageType.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrImageType.cs @@ -5,12 +5,8 @@ namespace SixLabors.ImageSharp.Formats.OpenExr { internal enum ExrImageType { - Unknown = 0, + ScanLine = 0, - Rgb = 1, - - Rgba = 2, - - Gray = 3, + Tiled = 1 } } From ea1aaa79c41c28d2c1bd886e7203e3467755ff39 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 18 Sep 2022 14:21:56 +0200 Subject: [PATCH 15/66] Fix build errors --- .../Compressors/NoneExrCompression.cs | 24 +- .../Compressors/RunLengthCompression.cs | 136 +- .../Compressors/ZipExrCompression.cs | 74 +- .../OpenExr/Compression/ExrBaseCompression.cs | 56 +- .../Compression/ExrBaseDecompressor.cs | 50 +- .../OpenExr/Compression/ExrCompressionType.cs | 113 +- .../Compression/ExrDecompressorFactory.cs | 33 +- .../Formats/OpenExr/ExrAttribute.cs | 32 +- src/ImageSharp/Formats/OpenExr/ExrBox2i.cs | 31 +- .../Formats/OpenExr/ExrChannelInfo.cs | 37 +- .../Formats/OpenExr/ExrConfigurationModule.cs | 25 +- .../Formats/OpenExr/ExrConstants.cs | 105 +- src/ImageSharp/Formats/OpenExr/ExrDecoder.cs | 91 +- .../Formats/OpenExr/ExrDecoderCore.cs | 1136 ++++++++--------- .../Formats/OpenExr/ExrDecoderOptions.cs | 13 + src/ImageSharp/Formats/OpenExr/ExrEncoder.cs | 48 +- .../Formats/OpenExr/ExrEncoderCore.cs | 633 +++++---- src/ImageSharp/Formats/OpenExr/ExrFormat.cs | 48 +- .../Formats/OpenExr/ExrHeaderAttributes.cs | 114 +- .../Formats/OpenExr/ExrImageDataType.cs | 17 +- .../Formats/OpenExr/ExrImageFormatDetector.cs | 38 +- .../Formats/OpenExr/ExrImageType.cs | 13 +- .../Formats/OpenExr/ExrLineOrder.cs | 15 +- src/ImageSharp/Formats/OpenExr/ExrMetadata.cs | 43 +- .../Formats/OpenExr/ExrPixelType.cs | 35 +- .../Formats/OpenExr/ExrThrowHelper.cs | 40 +- .../Formats/OpenExr/IExrDecoderOptions.cs | 12 - .../Formats/OpenExr/IExrEncoderOptions.cs | 19 +- .../Formats/OpenExr/MetadataExtensions.cs | 23 +- src/ImageSharp/IO/BufferedReadStream.cs | 26 + .../Formats/Exr/ImageExtensionsTest.cs | 163 +-- .../Formats/GeneralFormatTests.cs | 5 - 32 files changed, 1558 insertions(+), 1690 deletions(-) create mode 100644 src/ImageSharp/Formats/OpenExr/ExrDecoderOptions.cs delete mode 100644 src/ImageSharp/Formats/OpenExr/IExrDecoderOptions.cs diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs index d4eaf67ebb..333c453ebd 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs @@ -1,24 +1,22 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; + +internal class NoneExrCompression : ExrBaseDecompressor { - internal class NoneExrCompression : ExrBaseDecompressor + public NoneExrCompression(MemoryAllocator allocator, uint uncompressedBytes) + : base(allocator, uncompressedBytes) { - public NoneExrCompression(MemoryAllocator allocator, uint uncompressedBytes) - : base(allocator, uncompressedBytes) - { - } + } - public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) - => stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.UncompressedBytes)); + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + => stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.UncompressedBytes)); - protected override void Dispose(bool disposing) - { - } + protected override void Dispose(bool disposing) + { } } diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs index de1bf52328..08126c4de5 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs @@ -1,94 +1,92 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System; using System.Buffers; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; + +internal class RunLengthCompression : ExrBaseDecompressor { - internal class RunLengthCompression : ExrBaseDecompressor - { - private readonly IMemoryOwner tmpBuffer; + private readonly IMemoryOwner tmpBuffer; - public RunLengthCompression(MemoryAllocator allocator, uint uncompressedBytes) - : base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate((int)uncompressedBytes); + public RunLengthCompression(MemoryAllocator allocator, uint uncompressedBytes) + : base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate((int)uncompressedBytes); - public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + Span uncompressed = this.tmpBuffer.GetSpan(); + int maxLength = (int)this.UncompressedBytes; + int offset = 0; + while (compressedBytes > 0) { - Span uncompressed = this.tmpBuffer.GetSpan(); - int maxLength = (int)this.UncompressedBytes; - int offset = 0; - while (compressedBytes > 0) + byte nextByte = ReadNextByte(stream); + + sbyte input = (sbyte)nextByte; + if (input < 0) { - byte nextByte = ReadNextByte(stream); + int count = -input; + compressedBytes -= (uint)(count + 1); - sbyte input = (sbyte)nextByte; - if (input < 0) + if ((maxLength -= count) < 0) { - int count = -input; - compressedBytes -= (uint)(count + 1); - - if ((maxLength -= count) < 0) - { - return; - } - - // Check the input buffer is big enough to contain 'count' bytes of remaining data. - if (compressedBytes < 0) - { - return; - } - - for (int i = 0; i < count; i++) - { - uncompressed[offset + i] = ReadNextByte(stream); - } - - offset += count; + return; } - else + + // Check the input buffer is big enough to contain 'count' bytes of remaining data. + if (compressedBytes < 0) { - int count = input; - byte value = ReadNextByte(stream); - compressedBytes -= 2; - - if ((maxLength -= count + 1) < 0) - { - return; - } - - // Check the input buffer is big enough to contain byte to be duplicated. - if (compressedBytes < 0) - { - return; - } - - for (int i = 0; i < count + 1; i++) - { - uncompressed[offset + i] = value; - } - - offset += count + 1; + return; } - } - Reconstruct(uncompressed, this.UncompressedBytes); - Interleave(uncompressed, this.UncompressedBytes, buffer); - } + for (int i = 0; i < count; i++) + { + uncompressed[offset + i] = ReadNextByte(stream); + } - private static byte ReadNextByte(BufferedReadStream stream) - { - int nextByte = stream.ReadByte(); - if (nextByte == -1) + offset += count; + } + else { - ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to decompress RLE image!"); + int count = input; + byte value = ReadNextByte(stream); + compressedBytes -= 2; + + if ((maxLength -= count + 1) < 0) + { + return; + } + + // Check the input buffer is big enough to contain byte to be duplicated. + if (compressedBytes < 0) + { + return; + } + + for (int i = 0; i < count + 1; i++) + { + uncompressed[offset + i] = value; + } + + offset += count + 1; } + } + + Reconstruct(uncompressed, this.UncompressedBytes); + Interleave(uncompressed, this.UncompressedBytes, buffer); + } - return (byte)nextByte; + private static byte ReadNextByte(BufferedReadStream stream) + { + int nextByte = stream.ReadByte(); + if (nextByte == -1) + { + ExrThrowHelper.ThrowInvalidImageContentException("Not enough data to decompress RLE image!"); } - protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); + return (byte)nextByte; } + + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); } diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs index 65a2ea3485..d90f684e64 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs @@ -1,58 +1,56 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System; using System.Buffers; using System.IO.Compression; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; + +internal class ZipExrCompression : ExrBaseDecompressor { - internal class ZipExrCompression : ExrBaseDecompressor - { - private readonly IMemoryOwner tmpBuffer; + private readonly IMemoryOwner tmpBuffer; - public ZipExrCompression(MemoryAllocator allocator, uint uncompressedBytes) - : base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate((int)uncompressedBytes); + public ZipExrCompression(MemoryAllocator allocator, uint uncompressedBytes) + : base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate((int)uncompressedBytes); - public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + Span uncompressed = this.tmpBuffer.GetSpan(); + + long pos = stream.Position; + using ZlibInflateStream deframeStream = new( + stream, + () => + { + int left = (int)(compressedBytes - (stream.Position - pos)); + return left > 0 ? left : 0; + }); + deframeStream.AllocateNewBytes((int)this.UncompressedBytes, true); + DeflateStream dataStream = deframeStream.CompressedStream; + + int totalRead = 0; + while (totalRead < buffer.Length) { - Span uncompressed = this.tmpBuffer.GetSpan(); - - long pos = stream.Position; - using var deframeStream = new ZlibInflateStream( - stream, - () => - { - int left = (int)(compressedBytes - (stream.Position - pos)); - return left > 0 ? left : 0; - }); - deframeStream.AllocateNewBytes((int)this.UncompressedBytes, true); - DeflateStream dataStream = deframeStream.CompressedStream; - - int totalRead = 0; - while (totalRead < buffer.Length) + int bytesRead = dataStream.Read(uncompressed, totalRead, buffer.Length - totalRead); + if (bytesRead <= 0) { - int bytesRead = dataStream.Read(uncompressed, totalRead, buffer.Length - totalRead); - if (bytesRead <= 0) - { - break; - } - - totalRead += bytesRead; + break; } - if (totalRead == 0) - { - ExrThrowHelper.ThrowInvalidImageContentException("Could not read zip compressed image data!"); - } + totalRead += bytesRead; + } - Reconstruct(uncompressed, (uint)totalRead); - Interleave(uncompressed, (uint)totalRead, buffer); + if (totalRead == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read zip compressed image data!"); } - protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); + Reconstruct(uncompressed, (uint)totalRead); + Interleave(uncompressed, (uint)totalRead, buffer); } + + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); } diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs index 7faf4e77d6..4fd5778cb6 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs @@ -1,43 +1,41 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression; + +internal abstract class ExrBaseCompression : IDisposable { - internal abstract class ExrBaseCompression : IDisposable - { - private bool isDisposed; + private bool isDisposed; - protected ExrBaseCompression(MemoryAllocator allocator, uint bytePerRow) - { - this.Allocator = allocator; - this.UncompressedBytes = bytePerRow; - } + protected ExrBaseCompression(MemoryAllocator allocator, uint bytePerRow) + { + this.Allocator = allocator; + this.UncompressedBytes = bytePerRow; + } - /// - /// Gets the memory allocator. - /// - protected MemoryAllocator Allocator { get; } + /// + /// Gets the memory allocator. + /// + protected MemoryAllocator Allocator { get; } - /// - /// Gets the uncompressed bytes. - /// - public uint UncompressedBytes { get; } + /// + /// Gets the uncompressed bytes. + /// + public uint UncompressedBytes { get; } - /// - public void Dispose() + /// + public void Dispose() + { + if (this.isDisposed) { - if (this.isDisposed) - { - return; - } - - this.isDisposed = true; - this.Dispose(true); + return; } - protected abstract void Dispose(bool disposing); + this.isDisposed = true; + this.Dispose(true); } + + protected abstract void Dispose(bool disposing); } diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs index f8b811e502..2af814eb12 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs @@ -1,42 +1,40 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression; + +internal abstract class ExrBaseDecompressor : ExrBaseCompression { - internal abstract class ExrBaseDecompressor : ExrBaseCompression + protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytePerRow) + : base(allocator, bytePerRow) { - protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytePerRow) - : base(allocator, bytePerRow) - { - } + } - public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer); + public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer); - protected static void Reconstruct(Span buffer, uint unCompressedBytes) + protected static void Reconstruct(Span buffer, uint unCompressedBytes) + { + int offset = 0; + for (int i = 0; i < unCompressedBytes - 1; i++) { - int offset = 0; - for (int i = 0; i < unCompressedBytes - 1; i++) - { - byte d = (byte)(buffer[offset] + (buffer[offset + 1] - 128)); - buffer[offset + 1] = d; - offset++; - } + byte d = (byte)(buffer[offset] + (buffer[offset + 1] - 128)); + buffer[offset + 1] = d; + offset++; } + } - protected static void Interleave(Span source, uint unCompressedBytes, Span output) + protected static void Interleave(Span source, uint unCompressedBytes, Span output) + { + int sourceOffset = 0; + int offset0 = 0; + int offset1 = (int)((unCompressedBytes + 1) / 2); + while (sourceOffset < unCompressedBytes) { - int sourceOffset = 0; - int offset0 = 0; - int offset1 = (int)((unCompressedBytes + 1) / 2); - while (sourceOffset < unCompressedBytes) - { - output[sourceOffset++] = source[offset0++]; - output[sourceOffset++] = source[offset1++]; - } + output[sourceOffset++] = source[offset0++]; + output[sourceOffset++] = source[offset1++]; } } } diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrCompressionType.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrCompressionType.cs index 3b47ed6ecb..53fc2e2dc9 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/ExrCompressionType.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrCompressionType.cs @@ -1,61 +1,60 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression; + +internal enum ExrCompressionType { - internal enum ExrCompressionType - { - /// - /// 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 - } + /// + /// 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/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs index 8ba255d41c..2d01efd87b 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs @@ -1,28 +1,27 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression; + +internal static class ExrDecompressorFactory { - internal static class ExrDecompressorFactory + public static ExrBaseDecompressor Create(ExrCompressionType method, MemoryAllocator memoryAllocator, uint uncompressedBytes) { - public static ExrBaseDecompressor Create(ExrCompressionType method, MemoryAllocator memoryAllocator, uint uncompressedBytes) + switch (method) { - switch (method) - { - case ExrCompressionType.None: - return new NoneExrCompression(memoryAllocator, uncompressedBytes); - case ExrCompressionType.Zips: - return new ZipExrCompression(memoryAllocator, uncompressedBytes); - case ExrCompressionType.Zip: - return new ZipExrCompression(memoryAllocator, uncompressedBytes); - case ExrCompressionType.RunLengthEncoded: - return new RunLengthCompression(memoryAllocator, uncompressedBytes); - default: - throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)); - } + case ExrCompressionType.None: + return new NoneExrCompression(memoryAllocator, uncompressedBytes); + case ExrCompressionType.Zips: + return new ZipExrCompression(memoryAllocator, uncompressedBytes); + case ExrCompressionType.Zip: + return new ZipExrCompression(memoryAllocator, uncompressedBytes); + case ExrCompressionType.RunLengthEncoded: + return new RunLengthCompression(memoryAllocator, uncompressedBytes); + default: + throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)); } } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrAttribute.cs b/src/ImageSharp/Formats/OpenExr/ExrAttribute.cs index 66423f50dc..ad5a8290dc 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrAttribute.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrAttribute.cs @@ -1,26 +1,26 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License.. using System.Diagnostics; -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +[DebuggerDisplay("Name: {Name}, Type: {Type}, Length: {Length}")] +internal class ExrAttribute { - [DebuggerDisplay("Name: {Name}, Type: {Type}, Length: {Length}")] - internal class ExrAttribute - { - public static readonly ExrAttribute EmptyAttribute = new(string.Empty, string.Empty, 0); + 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 ExrAttribute(string name, string type, int length) + { + this.Name = name; + this.Type = type; + this.Length = length; + } - public string Name { get; } + public string Name { get; } - public string Type { get; } + public string Type { get; } - public int Length { get; } - } + public int Length { get; } } + diff --git a/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs b/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs index 89424b4259..1f5b0c0df8 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs @@ -1,27 +1,26 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. using System.Diagnostics; -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +[DebuggerDisplay("xMin: {XMin}, yMin: {YMin}, xMax: {XMax}, yMax: {YMax}")] +internal struct ExrBox2i { - [DebuggerDisplay("xMin: {XMin}, yMin: {YMin}, xMax: {XMax}, yMax: {YMax}")] - internal struct ExrBox2i + public ExrBox2i(int xMin, int yMin, int xMax, int yMax) { - 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/ExrChannelInfo.cs b/src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs index 28a014853d..d39bc08a5a 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs @@ -1,32 +1,31 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. using System.Diagnostics; using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +[DebuggerDisplay("Name: {ChannelName}, PixelType: {PixelType}")] +[StructLayout(LayoutKind.Sequential, Pack = 1)] +internal readonly struct ExrChannelInfo { - [DebuggerDisplay("Name: {ChannelName}, PixelType: {PixelType}")] - [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal readonly struct ExrChannelInfo + public ExrChannelInfo(string channelName, ExrPixelType pixelType, byte pLinear, int xSampling, int ySampling) { - 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; - } + this.ChannelName = channelName; + this.PixelType = pixelType; + this.PLinear = pLinear; + this.XSampling = xSampling; + this.YSampling = ySampling; + } - public string ChannelName { get; } + public string ChannelName { get; } - public ExrPixelType PixelType { get; } + public ExrPixelType PixelType { get; } - public byte PLinear { get; } + public byte PLinear { get; } - public int XSampling { get; } + public int XSampling { get; } - public int YSampling { get; } - } + public int YSampling { get; } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs b/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs index d5a03cd5dd..0b0058805d 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs @@ -1,19 +1,18 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +/// +/// Registers the image encoders, decoders and mime type detectors for the OpenExr format. +/// +public sealed class ExrConfigurationModule : IConfigurationModule { - /// - /// Registers the image encoders, decoders and mime type detectors for the OpenExr format. - /// - public sealed class ExrConfigurationModule : IConfigurationModule + /// + public void Configure(Configuration configuration) { - /// - public void Configure(Configuration configuration) - { - configuration.ImageFormatsManager.SetEncoder(ExrFormat.Instance, new ExrEncoder()); - configuration.ImageFormatsManager.SetDecoder(ExrFormat.Instance, new ExrDecoder()); - configuration.ImageFormatsManager.AddImageFormatDetector(new ExrImageFormatDetector()); - } + configuration.ImageFormatsManager.SetEncoder(ExrFormat.Instance, new ExrEncoder()); + 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 index 29adb7b887..42ccf27fbd 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrConstants.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrConstants.cs @@ -1,85 +1,82 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System.Collections.Generic; +namespace SixLabors.ImageSharp.Formats.OpenExr; -namespace SixLabors.ImageSharp.Formats.OpenExr +/// +/// Defines constants relating to OpenExr images. +/// +internal static class ExrConstants { /// - /// Defines constants relating to OpenExr images. + /// The list of mimetypes that equate to a OpenExr image. /// - internal static class ExrConstants - { - /// - /// The list of mimetypes that equate to a OpenExr image. - /// - public static readonly IEnumerable MimeTypes = new[] { "image/x-exr" }; + 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 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; + /// + /// The magick bytes identifying an OpenExr image. + /// + public static readonly int MagickBytes = 20000630; - /// - /// EXR attribute names. - /// - internal static class AttributeNames - { - public const string Channels = "channels"; + /// + /// EXR attribute names. + /// + internal static class AttributeNames + { + public const string Channels = "channels"; - public const string Compression = "compression"; + public const string Compression = "compression"; - public const string DataWindow = "dataWindow"; + public const string DataWindow = "dataWindow"; - public const string DisplayWindow = "displayWindow"; + public const string DisplayWindow = "displayWindow"; - public const string LineOrder = "lineOrder"; + public const string LineOrder = "lineOrder"; - public const string PixelAspectRatio = "pixelAspectRatio"; + public const string PixelAspectRatio = "pixelAspectRatio"; - public const string ScreenWindowCenter = "screenWindowCenter"; + public const string ScreenWindowCenter = "screenWindowCenter"; - public const string ScreenWindowWidth = "screenWindowWidth"; + public const string ScreenWindowWidth = "screenWindowWidth"; - public const string Tiles = "tiles"; + public const string Tiles = "tiles"; - public const string ChunkCount = "chunkCount"; - } + public const string ChunkCount = "chunkCount"; + } - /// - /// EXR attribute types. - /// - internal static class AttibuteTypes - { - public const string ChannelList = "chlist"; + /// + /// EXR attribute types. + /// + internal static class AttibuteTypes + { + public const string ChannelList = "chlist"; - public const string Compression = "compression"; + public const string Compression = "compression"; - public const string Float = "float"; + public const string Float = "float"; - public const string LineOrder = "lineOrder"; + public const string LineOrder = "lineOrder"; - public const string TwoFloat = "v2f"; + public const string TwoFloat = "v2f"; - public const string BoxInt = "box2i"; - } + public const string BoxInt = "box2i"; + } - internal static class ChannelNames - { - public const string Red = "R"; + internal static class ChannelNames + { + public const string Red = "R"; - public const string Green = "G"; + public const string Green = "G"; - public const string Blue = "B"; + public const string Blue = "B"; - public const string Alpha = "A"; + public const string Alpha = "A"; - public const string Luminance = "Y"; - } + public const string Luminance = "Y"; } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoder.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoder.cs index 0c645d71e1..dcbe80c277 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoder.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoder.cs @@ -1,61 +1,46 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System.IO; -using System.Threading; -using System.Threading.Tasks; using SixLabors.ImageSharp.PixelFormats; -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +/// +/// Image decoder for generating an image out of a OpenExr stream. +/// +public class ExrDecoder : IImageDecoderSpecialized { - /// - /// Image decoder for generating an image out of a OpenExr stream. - /// - public sealed class ExrDecoder : IImageDecoder, IExrDecoderOptions, IImageInfoDetector + /// + IImageInfo IImageInfoDetector.Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { - /// - 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); - } + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + return new ExrDecoderCore(new() { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken); } + + /// + Image IImageDecoder.Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => ((IImageDecoderSpecialized)this).Decode(new() { GeneralOptions = options }, stream, cancellationToken); + + /// + Image IImageDecoder.Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => ((IImageDecoderSpecialized)this).Decode(new() { GeneralOptions = options }, stream, cancellationToken); + + /// + Image IImageDecoderSpecialized.Decode(ExrDecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + Image image = new ExrDecoderCore(options).Decode(options.GeneralOptions.Configuration, stream, cancellationToken); + + ImageDecoderUtilities.Resize(options.GeneralOptions, image); + + return image; + } + + /// + Image IImageDecoderSpecialized.Decode(ExrDecoderOptions options, Stream stream, CancellationToken cancellationToken) + => ((IImageDecoderSpecialized)this).Decode(options, stream, cancellationToken); } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index d414babf28..ca244c5674 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -1,765 +1,759 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System; using System.Buffers; using System.Buffers.Binary; -using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; -using System.Threading; using SixLabors.ImageSharp.Formats.OpenExr.Compression; -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 +namespace SixLabors.ImageSharp.Formats.OpenExr; + +/// +/// Performs the OpenExr decoding operation. +/// +internal sealed class ExrDecoderCore : IImageDecoderInternals { /// - /// Performs the OpenExr decoding operation. + /// Reusable buffer. /// - internal sealed class ExrDecoderCore : IImageDecoderInternals - { - /// - /// Reusable buffer. - /// - private readonly byte[] buffer = new byte[8]; - - /// - /// Used for allocating memory during processing operations. - /// - private readonly MemoryAllocator memoryAllocator; + private readonly byte[] buffer = new byte[8]; - /// - /// The bitmap decoder options. - /// - private readonly IExrDecoderOptions options; + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; - /// - /// The metadata. - /// - private ImageMetadata metadata; + /// + /// The global configuration. + /// + private readonly Configuration configuration; - /// - /// 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; - } + /// + /// The metadata. + /// + private ImageMetadata metadata; - /// - public Configuration Configuration { get; } + /// + /// Initializes a new instance of the class. + /// + /// The options. + public ExrDecoderCore(ExrDecoderOptions options) + { + this.Options = options.GeneralOptions; + this.configuration = options.GeneralOptions.Configuration; + this.memoryAllocator = this.configuration.MemoryAllocator; + } - /// - /// Gets the dimensions of the image. - /// - public Size Dimensions => new(this.Width, this.Height); + /// + public DecoderOptions Options { get; } - private int Width { get; set; } + /// + /// Gets the dimensions of the image. + /// + public Size Dimensions => new(this.Width, this.Height); - private int Height { get; set; } + private int Width { get; set; } - private IList Channels { get; set; } + private int Height { get; set; } - private ExrCompressionType Compression { get; set; } + private IList Channels { get; set; } - private ExrImageDataType ImageDataType { get; set; } + private ExrCompressionType Compression { get; set; } - private ExrImageType ImageType { get; set; } + private ExrImageDataType ImageDataType { get; set; } - private ExrHeaderAttributes HeaderAttributes { get; set; } + private ExrImageType ImageType { get; set; } - /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - this.ReadExrHeader(stream); - this.IsSupportedCompression(); - ExrPixelType pixelType = this.ValidateChannels(); - this.ReadImageDataType(); + private ExrHeaderAttributes HeaderAttributes { get; set; } - var image = new Image(this.Configuration, this.Width, this.Height, this.metadata); - Buffer2D pixels = image.GetRootFramePixelBuffer(); + /// + public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + this.ReadExrHeader(stream); + this.IsSupportedCompression(); + ExrPixelType pixelType = this.ValidateChannels(); + this.ReadImageDataType(); - switch (pixelType) - { - case ExrPixelType.Half: - case ExrPixelType.Float: - this.DecodeFloatingPointPixelData(stream, pixels); - break; - case ExrPixelType.UnsignedInt: - this.DecodeUnsignedIntPixelData(stream, pixels); - break; - default: - ExrThrowHelper.ThrowNotSupported("Pixel type is not supported"); - break; - } + Image image = new Image(this.configuration, this.Width, this.Height, this.metadata); + Buffer2D pixels = image.GetRootFramePixelBuffer(); - return image; + switch (pixelType) + { + case ExrPixelType.Half: + case ExrPixelType.Float: + this.DecodeFloatingPointPixelData(stream, pixels); + break; + case ExrPixelType.UnsignedInt: + this.DecodeUnsignedIntPixelData(stream, pixels); + break; + default: + ExrThrowHelper.ThrowNotSupported("Pixel type is not supported"); + break; } - private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buffer2D pixels) - where TPixel : unmanaged, IPixel + return image; + } + + private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buffer2D pixels) + where TPixel : unmanaged, IPixel + { + bool hasAlpha = this.HasAlpha(); + uint bytesPerRow = this.CalculateBytesPerRow((uint)this.Width); + uint rowsPerBlock = this.RowsPerBlock(); + uint bytesPerBlock = bytesPerRow * rowsPerBlock; + int width = this.Width; + int height = this.Height; + + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); + using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); + Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); + Span redPixelData = rowBuffer.GetSpan().Slice(0, width); + Span greenPixelData = rowBuffer.GetSpan().Slice(width, width); + Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); + Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); + + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock); + + TPixel color = default; + for (uint y = 0; y < height; y += rowsPerBlock) { - bool hasAlpha = this.HasAlpha(); - uint bytesPerRow = this.CalculateBytesPerRow((uint)this.Width); - uint rowsPerBlock = this.RowsPerBlock(); - uint bytesPerBlock = bytesPerRow * rowsPerBlock; - int width = this.Width; - int height = this.Height; + ulong rowOffset = this.ReadUnsignedLong(stream); + long nextRowOffsetPosition = stream.Position; - using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); - using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); - Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); - Span redPixelData = rowBuffer.GetSpan().Slice(0, width); - Span greenPixelData = rowBuffer.GetSpan().Slice(width, width); - Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); - Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); + stream.Position = (long)rowOffset; + uint rowStartIndex = this.ReadUnsignedInteger(stream); - using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock); + uint compressedBytesCount = this.ReadUnsignedInteger(stream); + decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); - TPixel color = default; - for (uint y = 0; y < height; y += rowsPerBlock) + int offset = 0; + for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) { - ulong rowOffset = this.ReadUnsignedLong(stream); - long nextRowOffsetPosition = stream.Position; - - stream.Position = (long)rowOffset; - uint rowStartIndex = this.ReadUnsignedInteger(stream); - - uint compressedBytesCount = this.ReadUnsignedInteger(stream); - decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); - - int offset = 0; - for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) + Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); + for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { - Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); - for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) - { - ExrChannelInfo channel = this.Channels[channelIdx]; - offset += this.ReadFloatChannelData(stream, channel, decompressedPixelData.Slice(offset), redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); - } - - for (int x = 0; x < width; x++) - { - var pixelValue = new HalfVector4(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1.0f); - color.FromVector4(pixelValue.ToVector4()); - pixelRow[x] = color; - } + ExrChannelInfo channel = this.Channels[channelIdx]; + offset += this.ReadFloatChannelData(stream, channel, decompressedPixelData.Slice(offset), redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); + } + for (int x = 0; x < width; x++) + { + var pixelValue = new HalfVector4(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1.0f); + color.FromVector4(pixelValue.ToVector4()); + pixelRow[x] = color; } - stream.Position = nextRowOffsetPosition; } + + stream.Position = nextRowOffsetPosition; } + } - private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffer2D pixels) - where TPixel : unmanaged, IPixel + private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffer2D pixels) + where TPixel : unmanaged, IPixel + { + bool hasAlpha = this.HasAlpha(); + uint bytesPerRow = this.CalculateBytesPerRow((uint)this.Width); + uint rowsPerBlock = this.RowsPerBlock(); + uint bytesPerBlock = bytesPerRow * rowsPerBlock; + int width = this.Width; + int height = this.Height; + + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); + using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); + Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); + Span redPixelData = rowBuffer.GetSpan().Slice(0, width); + Span greenPixelData = rowBuffer.GetSpan().Slice(width, width); + Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); + Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); + + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock); + + TPixel color = default; + for (uint y = 0; y < height; y += rowsPerBlock) { - bool hasAlpha = this.HasAlpha(); - uint bytesPerRow = this.CalculateBytesPerRow((uint)this.Width); - uint rowsPerBlock = this.RowsPerBlock(); - uint bytesPerBlock = bytesPerRow * rowsPerBlock; - int width = this.Width; - int height = this.Height; + ulong rowOffset = this.ReadUnsignedLong(stream); + long nextRowOffsetPosition = stream.Position; - using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); - using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); - Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); - Span redPixelData = rowBuffer.GetSpan().Slice(0, width); - Span greenPixelData = rowBuffer.GetSpan().Slice(width, width); - Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); - Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); + stream.Position = (long)rowOffset; + uint rowStartIndex = this.ReadUnsignedInteger(stream); - using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock); + uint compressedBytesCount = this.ReadUnsignedInteger(stream); + decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); - TPixel color = default; - for (uint y = 0; y < height; y += rowsPerBlock) + int offset = 0; + for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) { - ulong rowOffset = this.ReadUnsignedLong(stream); - long nextRowOffsetPosition = stream.Position; - - stream.Position = (long)rowOffset; - uint rowStartIndex = this.ReadUnsignedInteger(stream); - - uint compressedBytesCount = this.ReadUnsignedInteger(stream); - decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); - - int offset = 0; - for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) + Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); + for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { - Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); - for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) - { - ExrChannelInfo channel = this.Channels[channelIdx]; - offset += this.ReadUnsignedIntChannelData(stream, channel, decompressedPixelData, redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); - } + ExrChannelInfo channel = this.Channels[channelIdx]; + offset += this.ReadUnsignedIntChannelData(stream, channel, decompressedPixelData, redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); + } - stream.Position = nextRowOffsetPosition; + stream.Position = nextRowOffsetPosition; - for (int x = 0; x < width; x++) - { - var pixelValue = new Rgba128(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue); - color.FromVector4(pixelValue.ToVector4()); - pixelRow[x] = color; - } + for (int x = 0; x < width; x++) + { + var pixelValue = new Rgba128(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue); + color.FromVector4(pixelValue.ToVector4()); + pixelRow[x] = color; } } } + } - private int ReadFloatChannelData( - BufferedReadStream stream, - ExrChannelInfo channel, - Span decompressedPixelData, - Span redPixelData, - Span greenPixelData, - Span bluePixelData, - Span alphaPixelData, - int width) + private int ReadFloatChannelData( + BufferedReadStream stream, + ExrChannelInfo channel, + Span decompressedPixelData, + Span redPixelData, + Span greenPixelData, + Span bluePixelData, + Span alphaPixelData, + int width) + { + switch (channel.ChannelName) { - switch (channel.ChannelName) - { - case ExrConstants.ChannelNames.Red: - return this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); + case ExrConstants.ChannelNames.Red: + return this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); - case ExrConstants.ChannelNames.Blue: - return this.ReadChannelData(channel, decompressedPixelData, bluePixelData, width); + case ExrConstants.ChannelNames.Blue: + return this.ReadChannelData(channel, decompressedPixelData, bluePixelData, width); - case ExrConstants.ChannelNames.Green: - return this.ReadChannelData(channel, decompressedPixelData, greenPixelData, width); + case ExrConstants.ChannelNames.Green: + return this.ReadChannelData(channel, decompressedPixelData, greenPixelData, width); - case ExrConstants.ChannelNames.Alpha: - return this.ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); + case ExrConstants.ChannelNames.Alpha: + return this.ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); - case ExrConstants.ChannelNames.Luminance: - int bytesRead = this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); - redPixelData.CopyTo(bluePixelData); - redPixelData.CopyTo(greenPixelData); + case ExrConstants.ChannelNames.Luminance: + int bytesRead = this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); + redPixelData.CopyTo(bluePixelData); + redPixelData.CopyTo(greenPixelData); - return bytesRead; + return bytesRead; - default: - // Skip unknown channel. - int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; - stream.Position += width * channelDataSizeInBytes; - return channelDataSizeInBytes; - } + default: + // Skip unknown channel. + int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; + stream.Position += width * channelDataSizeInBytes; + return channelDataSizeInBytes; } + } - private int ReadUnsignedIntChannelData( - BufferedReadStream stream, - ExrChannelInfo channel, - Span decompressedPixelData, - Span redPixelData, - Span greenPixelData, - Span bluePixelData, - Span alphaPixelData, - int width) + private int ReadUnsignedIntChannelData( + BufferedReadStream stream, + ExrChannelInfo channel, + Span decompressedPixelData, + Span redPixelData, + Span greenPixelData, + Span bluePixelData, + Span alphaPixelData, + int width) + { + switch (channel.ChannelName) { - switch (channel.ChannelName) - { - case ExrConstants.ChannelNames.Red: - return this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); + case ExrConstants.ChannelNames.Red: + return this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); - case ExrConstants.ChannelNames.Blue: - return this.ReadChannelData(channel, decompressedPixelData, bluePixelData, width); + case ExrConstants.ChannelNames.Blue: + return this.ReadChannelData(channel, decompressedPixelData, bluePixelData, width); - case ExrConstants.ChannelNames.Green: - return this.ReadChannelData(channel, decompressedPixelData, greenPixelData, width); + case ExrConstants.ChannelNames.Green: + return this.ReadChannelData(channel, decompressedPixelData, greenPixelData, width); - case ExrConstants.ChannelNames.Alpha: - return this.ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); + case ExrConstants.ChannelNames.Alpha: + return this.ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); - case ExrConstants.ChannelNames.Luminance: - int bytesRead = this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); - redPixelData.CopyTo(bluePixelData); - redPixelData.CopyTo(greenPixelData); - return bytesRead; + case ExrConstants.ChannelNames.Luminance: + int bytesRead = this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); + redPixelData.CopyTo(bluePixelData); + redPixelData.CopyTo(greenPixelData); + return bytesRead; - default: - // Skip unknown channel. - int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; - stream.Position += this.Width * channelDataSizeInBytes; - return channelDataSizeInBytes; - } + default: + // Skip unknown channel. + int channelDataSizeInBytes = channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt ? 4 : 2; + stream.Position += this.Width * channelDataSizeInBytes; + return channelDataSizeInBytes; } + } - private int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) + private int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) + { + switch (channel.PixelType) { - switch (channel.PixelType) - { - case ExrPixelType.Half: - return this.ReadPixelRowChannelHalfSingle(decompressedPixelData, pixelData, width); - case ExrPixelType.Float: - return this.ReadPixelRowChannelSingle(decompressedPixelData, pixelData, width); - } - - return 0; + case ExrPixelType.Half: + return this.ReadPixelRowChannelHalfSingle(decompressedPixelData, pixelData, width); + case ExrPixelType.Float: + return this.ReadPixelRowChannelSingle(decompressedPixelData, pixelData, width); } - private int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) - { - switch (channel.PixelType) - { - case ExrPixelType.UnsignedInt: - return this.ReadPixelRowChannelUnsignedInt(decompressedPixelData, pixelData, width); - } + return 0; + } - return 0; + private int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) + { + switch (channel.PixelType) + { + case ExrPixelType.UnsignedInt: + return this.ReadPixelRowChannelUnsignedInt(decompressedPixelData, pixelData, width); } - private int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span channelData, int width) - { - int offset = 0; - for (int x = 0; x < width; x++) - { - ushort shortValue = BinaryPrimitives.ReadUInt16LittleEndian(decompressedPixelData.Slice(offset, 2)); - channelData[x] = HalfTypeHelper.Unpack(shortValue); - offset += 2; - } + return 0; + } - return offset; + private int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span channelData, int width) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + ushort shortValue = BinaryPrimitives.ReadUInt16LittleEndian(decompressedPixelData.Slice(offset, 2)); + channelData[x] = HalfTypeHelper.Unpack(shortValue); + offset += 2; } - private int ReadPixelRowChannelSingle(Span decompressedPixelData, Span channelData, int width) - { - int offset = 0; - for (int x = 0; x < width; x++) - { - int intValue = BinaryPrimitives.ReadInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); - channelData[x] = Unsafe.As(ref intValue); - offset += 4; - } + return offset; + } - return offset; + private int ReadPixelRowChannelSingle(Span decompressedPixelData, Span channelData, int width) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + int intValue = BinaryPrimitives.ReadInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); + channelData[x] = Unsafe.As(ref intValue); + offset += 4; } - private int ReadPixelRowChannelUnsignedInt(Span decompressedPixelData, Span channelData, int width) - { - int offset = 0; - for (int x = 0; x < width; x++) - { - channelData[x] = BinaryPrimitives.ReadUInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); - offset += 4; - } + return offset; + } - return offset; + private int ReadPixelRowChannelUnsignedInt(Span decompressedPixelData, Span channelData, int width) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + channelData[x] = BinaryPrimitives.ReadUInt32LittleEndian(decompressedPixelData.Slice(offset, 4)); + offset += 4; } - /// - public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) - { - ExrHeaderAttributes header = this.ReadExrHeader(stream); + return offset; + } + + /// + public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + ExrHeaderAttributes header = this.ReadExrHeader(stream); - int bitsPerPixel = this.CalculateBitsPerPixel(); + int bitsPerPixel = this.CalculateBitsPerPixel(); - return new ImageInfo(new PixelTypeInfo(bitsPerPixel), this.Width, this.Height, new ImageMetadata()); - } + return new ImageInfo(new PixelTypeInfo(bitsPerPixel), this.Width, this.Height, new ImageMetadata()); + } - private int CalculateBitsPerPixel() + private int CalculateBitsPerPixel() + { + int bitsPerPixel = 0; + for (int i = 0; i < this.Channels.Count; i++) { - 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) { - ExrChannelInfo channel = this.Channels[0]; - if (channel.PixelType is ExrPixelType.Float or ExrPixelType.UnsignedInt) - { - bitsPerPixel += 32; - } - else if (channel.PixelType == ExrPixelType.Half) - { - bitsPerPixel += 16; - } + bitsPerPixel += 32; } - - return bitsPerPixel; - } - - private ExrPixelType ValidateChannels() - { - if (this.Channels.Count == 0) + else if (channel.PixelType == ExrPixelType.Half) { - ExrThrowHelper.ThrowInvalidImageContentException("At least one channel of pixel data is expected!"); + bitsPerPixel += 16; } - - // Find pixel the type of any channel which is R, G, B or A. - ExrPixelType pixelType = this.FindPixelType(); - - return pixelType; } - private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) + return bitsPerPixel; + } + + private ExrPixelType ValidateChannels() + { + if (this.Channels.Count == 0) { - // Skip over the magick bytes, we already know its an EXR image. - stream.Skip(4); + ExrThrowHelper.ThrowInvalidImageContentException("At least one channel of pixel data is expected!"); + } - // Read version number. - byte version = (byte)stream.ReadByte(); - if (version != 2) - { - ExrThrowHelper.ThrowNotSupportedVersion(); - } + // Find pixel the type of any channel which is R, G, B or A. + ExrPixelType pixelType = this.FindPixelType(); - // Next three bytes contain info's about the image. - byte flagsByte0 = (byte)stream.ReadByte(); - byte flagsByte1 = (byte)stream.ReadByte(); - byte flagsByte2 = (byte)stream.ReadByte(); - if ((flagsByte0 & (1 << 1)) != 0) - { - this.ImageType = ExrImageType.Tiled; - } + return pixelType; + } - this.HeaderAttributes = this.ParseHeaderAttributes(stream); + private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) + { + // Skip over the magick bytes, we already know its an EXR image. + stream.Skip(4); - if (!this.HeaderAttributes.IsValid()) - { - ExrThrowHelper.ThrowInvalidImageHeader(); - } + // Read version number. + byte version = (byte)stream.ReadByte(); + if (version != 2) + { + ExrThrowHelper.ThrowNotSupportedVersion(); + } - this.Width = this.HeaderAttributes.DataWindow.Value.XMax - this.HeaderAttributes.DataWindow.Value.XMin + 1; - this.Height = this.HeaderAttributes.DataWindow.Value.YMax - this.HeaderAttributes.DataWindow.Value.YMin + 1; - this.Channels = this.HeaderAttributes.Channels; - this.Compression = this.HeaderAttributes.Compression.GetValueOrDefault(); + // Next three bytes contain info's about the image. + byte flagsByte0 = (byte)stream.ReadByte(); + byte flagsByte1 = (byte)stream.ReadByte(); + byte flagsByte2 = (byte)stream.ReadByte(); + if ((flagsByte0 & (1 << 1)) != 0) + { + this.ImageType = ExrImageType.Tiled; + } - this.metadata = new ImageMetadata(); + this.HeaderAttributes = this.ParseHeaderAttributes(stream); - return this.HeaderAttributes; + if (!this.HeaderAttributes.IsValid()) + { + ExrThrowHelper.ThrowInvalidImageHeader(); } - private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) - { - ExrAttribute attribute = this.ReadAttribute(stream); - var header = new ExrHeaderAttributes(); + this.Width = this.HeaderAttributes.DataWindow.Value.XMax - this.HeaderAttributes.DataWindow.Value.XMin + 1; + this.Height = this.HeaderAttributes.DataWindow.Value.YMax - this.HeaderAttributes.DataWindow.Value.YMin + 1; + this.Channels = this.HeaderAttributes.Channels; + this.Compression = this.HeaderAttributes.Compression.GetValueOrDefault(); - while (!attribute.Equals(ExrAttribute.EmptyAttribute)) - { - switch (attribute.Name) - { - case ExrConstants.AttributeNames.Channels: - IList channels = this.ReadChannelList(stream, attribute.Length); - header.Channels = channels; - break; - case ExrConstants.AttributeNames.Compression: - header.Compression = (ExrCompressionType)stream.ReadByte(); - break; - case ExrConstants.AttributeNames.DataWindow: - ExrBox2i dataWindow = this.ReadBoxInteger(stream); - header.DataWindow = dataWindow; - break; - case ExrConstants.AttributeNames.DisplayWindow: - ExrBox2i displayWindow = this.ReadBoxInteger(stream); - header.DisplayWindow = displayWindow; - break; - case ExrConstants.AttributeNames.LineOrder: - var lineOrder = (ExrLineOrder)stream.ReadByte(); - header.LineOrder = lineOrder; - break; - case ExrConstants.AttributeNames.PixelAspectRatio: - float aspectRatio = stream.ReadSingle(this.buffer); - header.AspectRatio = aspectRatio; - break; - case ExrConstants.AttributeNames.ScreenWindowCenter: - float screenWindowCenterX = stream.ReadSingle(this.buffer); - float screenWindowCenterY = stream.ReadSingle(this.buffer); - header.ScreenWindowCenter = new PointF(screenWindowCenterX, screenWindowCenterY); - break; - case ExrConstants.AttributeNames.ScreenWindowWidth: - float screenWindowWidth = stream.ReadSingle(this.buffer); - header.ScreenWindowWidth = screenWindowWidth; - break; - case ExrConstants.AttributeNames.Tiles: - header.TileXSize = this.ReadUnsignedInteger(stream); - header.TileYSize = this.ReadUnsignedInteger(stream); - break; - case ExrConstants.AttributeNames.ChunkCount: - header.ChunkCount = this.ReadSignedInteger(stream); - break; - default: - // Skip unknown attribute bytes. - stream.Skip(attribute.Length); - break; - } + this.metadata = new ImageMetadata(); - attribute = this.ReadAttribute(stream); - } + return this.HeaderAttributes; + } - return header; - } + private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) + { + ExrAttribute attribute = this.ReadAttribute(stream); + var header = new ExrHeaderAttributes(); - private ExrAttribute ReadAttribute(BufferedReadStream stream) + while (!attribute.Equals(ExrAttribute.EmptyAttribute)) { - string attributeName = ReadString(stream); - if (attributeName.Equals(string.Empty)) + switch (attribute.Name) { - return ExrAttribute.EmptyAttribute; + case ExrConstants.AttributeNames.Channels: + IList channels = this.ReadChannelList(stream, attribute.Length); + header.Channels = channels; + break; + case ExrConstants.AttributeNames.Compression: + header.Compression = (ExrCompressionType)stream.ReadByte(); + break; + case ExrConstants.AttributeNames.DataWindow: + ExrBox2i dataWindow = this.ReadBoxInteger(stream); + header.DataWindow = dataWindow; + break; + case ExrConstants.AttributeNames.DisplayWindow: + ExrBox2i displayWindow = this.ReadBoxInteger(stream); + header.DisplayWindow = displayWindow; + break; + case ExrConstants.AttributeNames.LineOrder: + var lineOrder = (ExrLineOrder)stream.ReadByte(); + header.LineOrder = lineOrder; + break; + case ExrConstants.AttributeNames.PixelAspectRatio: + float aspectRatio = stream.ReadSingle(this.buffer); + header.AspectRatio = aspectRatio; + break; + case ExrConstants.AttributeNames.ScreenWindowCenter: + float screenWindowCenterX = stream.ReadSingle(this.buffer); + float screenWindowCenterY = stream.ReadSingle(this.buffer); + header.ScreenWindowCenter = new PointF(screenWindowCenterX, screenWindowCenterY); + break; + case ExrConstants.AttributeNames.ScreenWindowWidth: + float screenWindowWidth = stream.ReadSingle(this.buffer); + header.ScreenWindowWidth = screenWindowWidth; + break; + case ExrConstants.AttributeNames.Tiles: + header.TileXSize = this.ReadUnsignedInteger(stream); + header.TileYSize = this.ReadUnsignedInteger(stream); + break; + case ExrConstants.AttributeNames.ChunkCount: + header.ChunkCount = this.ReadSignedInteger(stream); + break; + default: + // Skip unknown attribute bytes. + stream.Skip(attribute.Length); + break; } - string attributeType = ReadString(stream); + attribute = this.ReadAttribute(stream); + } - int attributeSize = this.ReadSignedInteger(stream); + return header; + } - return new ExrAttribute(attributeName, attributeType, attributeSize); + private ExrAttribute ReadAttribute(BufferedReadStream stream) + { + string attributeName = ReadString(stream); + if (attributeName.Equals(string.Empty)) + { + return ExrAttribute.EmptyAttribute; } - private ExrBox2i ReadBoxInteger(BufferedReadStream stream) - { - int xMin = this.ReadSignedInteger(stream); - int yMin = this.ReadSignedInteger(stream); - int xMax = this.ReadSignedInteger(stream); - int yMax = this.ReadSignedInteger(stream); + string attributeType = ReadString(stream); - return new ExrBox2i(xMin, yMin, xMax, yMax); - } + int attributeSize = this.ReadSignedInteger(stream); - private List ReadChannelList(BufferedReadStream stream, int attributeSize) - { - var channels = new List(); - while (attributeSize > 1) - { - ExrChannelInfo channelInfo = this.ReadChannelInfo(stream, out int bytesRead); - channels.Add(channelInfo); - attributeSize -= bytesRead; - } + return new ExrAttribute(attributeName, attributeType, attributeSize); + } - // Last byte should be a null byte. - int byteRead = stream.ReadByte(); + private ExrBox2i ReadBoxInteger(BufferedReadStream stream) + { + int xMin = this.ReadSignedInteger(stream); + int yMin = this.ReadSignedInteger(stream); + int xMax = this.ReadSignedInteger(stream); + int yMax = this.ReadSignedInteger(stream); - return channels; - } + return new ExrBox2i(xMin, yMin, xMax, yMax); + } - private ExrChannelInfo ReadChannelInfo(BufferedReadStream stream, out int bytesRead) + private List ReadChannelList(BufferedReadStream stream, int attributeSize) + { + var channels = new List(); + while (attributeSize > 1) { - string channelName = ReadString(stream); - bytesRead = channelName.Length + 1; + ExrChannelInfo channelInfo = this.ReadChannelInfo(stream, out int bytesRead); + channels.Add(channelInfo); + attributeSize -= bytesRead; + } - var pixelType = (ExrPixelType)this.ReadSignedInteger(stream); - bytesRead += 4; + // Last byte should be a null byte. + int byteRead = stream.ReadByte(); - byte pLinear = (byte)stream.ReadByte(); - byte reserved0 = (byte)stream.ReadByte(); - byte reserved1 = (byte)stream.ReadByte(); - byte reserved2 = (byte)stream.ReadByte(); - bytesRead += 4; + return channels; + } - int xSampling = this.ReadSignedInteger(stream); - bytesRead += 4; + private ExrChannelInfo ReadChannelInfo(BufferedReadStream stream, out int bytesRead) + { + string channelName = ReadString(stream); + bytesRead = channelName.Length + 1; - int ySampling = this.ReadSignedInteger(stream); - bytesRead += 4; + var pixelType = (ExrPixelType)this.ReadSignedInteger(stream); + bytesRead += 4; - return new ExrChannelInfo(channelName, pixelType, pLinear, xSampling, ySampling); - } + byte pLinear = (byte)stream.ReadByte(); + byte reserved0 = (byte)stream.ReadByte(); + byte reserved1 = (byte)stream.ReadByte(); + byte reserved2 = (byte)stream.ReadByte(); + bytesRead += 4; + + int xSampling = this.ReadSignedInteger(stream); + bytesRead += 4; + + int ySampling = this.ReadSignedInteger(stream); + bytesRead += 4; - private static string ReadString(BufferedReadStream stream) + 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) { - var str = new StringBuilder(); - int character = stream.ReadByte(); - if (character == 0) - { - // End of file header reached. - return string.Empty; - } + // End of file header reached. + return string.Empty; + } - while (character != 0) + while (character != 0) + { + if (character == -1) { - if (character == -1) - { - ExrThrowHelper.ThrowInvalidImageHeader(); - } - - str.Append((char)character); - character = stream.ReadByte(); + ExrThrowHelper.ThrowInvalidImageHeader(); } - return str.ToString(); + str.Append((char)character); + character = stream.ReadByte(); } - private ExrPixelType FindPixelType() + return str.ToString(); + } + + private ExrPixelType FindPixelType() + { + ExrPixelType? pixelType = null; + for (int i = 0; i < this.Channels.Count; i++) { - 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) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Luminance)) { - 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) || - this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Luminance)) + if (!pixelType.HasValue) { - if (!pixelType.HasValue) - { - pixelType = this.Channels[i].PixelType; - } - else + pixelType = this.Channels[i].PixelType; + } + else + { + if (pixelType != this.Channels[i].PixelType) { - if (pixelType != this.Channels[i].PixelType) - { - ExrThrowHelper.ThrowNotSupported("Pixel channel data is expected to be the same for all channels."); - } + ExrThrowHelper.ThrowNotSupported("Pixel channel data is expected to be the same for all channels."); } } } + } - if (!pixelType.HasValue) - { - ExrThrowHelper.ThrowNotSupported("Pixel channel data is unknown! Only R, G, B, A and Y are supported."); - } - - return pixelType.Value; + if (!pixelType.HasValue) + { + ExrThrowHelper.ThrowNotSupported("Pixel channel data is unknown! Only R, G, B, A and Y are supported."); } - private void IsSupportedCompression() + return pixelType.Value; + } + + private void IsSupportedCompression() + { + if (this.Compression is not ExrCompressionType.None and not ExrCompressionType.Zips and not ExrCompressionType.Zip and not ExrCompressionType.RunLengthEncoded) { - if (this.Compression is not ExrCompressionType.None and not ExrCompressionType.Zips and not ExrCompressionType.Zip and not ExrCompressionType.RunLengthEncoded) - { - ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); - } + ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); } + } - private void ReadImageDataType() + private void ReadImageDataType() + { + bool hasRedChannel = false; + bool hasGreenChannel = false; + bool hasBlueChannel = false; + bool hasAlphaChannel = false; + bool hasLuminance = false; + foreach (ExrChannelInfo channelInfo in this.Channels) { - bool hasRedChannel = false; - bool hasGreenChannel = false; - bool hasBlueChannel = false; - bool hasAlphaChannel = false; - bool hasLuminance = false; - foreach (ExrChannelInfo channelInfo in this.Channels) + if (channelInfo.ChannelName.Equals("A")) { - if (channelInfo.ChannelName.Equals("A")) - { - hasAlphaChannel = true; - } - - if (channelInfo.ChannelName.Equals("R")) - { - hasRedChannel = true; - } - - if (channelInfo.ChannelName.Equals("G")) - { - hasGreenChannel = true; - } - - if (channelInfo.ChannelName.Equals("B")) - { - hasBlueChannel = true; - } - - if (channelInfo.ChannelName.Equals("Y")) - { - hasLuminance = true; - } + hasAlphaChannel = true; } - if (hasRedChannel && hasGreenChannel && hasBlueChannel && hasAlphaChannel) + if (channelInfo.ChannelName.Equals("R")) { - this.ImageDataType = ExrImageDataType.Rgba; - return; + hasRedChannel = true; } - if (hasRedChannel && hasGreenChannel && hasBlueChannel) + if (channelInfo.ChannelName.Equals("G")) { - this.ImageDataType = ExrImageDataType.Rgb; - return; + hasGreenChannel = true; } - if (hasLuminance && this.Channels.Count == 1) + if (channelInfo.ChannelName.Equals("B")) { - this.ImageDataType = ExrImageDataType.Gray; - return; + hasBlueChannel = true; } - ExrThrowHelper.ThrowNotSupported("The image contains channels, which are not supported!"); - } - - private bool HasAlpha() - { - foreach (ExrChannelInfo channelInfo in this.Channels) + if (channelInfo.ChannelName.Equals("Y")) { - if (channelInfo.ChannelName.Equals("A")) - { - return true; - } + hasLuminance = true; } + } - return false; + if (hasRedChannel && hasGreenChannel && hasBlueChannel && hasAlphaChannel) + { + this.ImageDataType = ExrImageDataType.Rgba; + return; } - private uint CalculateBytesPerRow(uint width) + if (hasRedChannel && hasGreenChannel && hasBlueChannel) { - uint bytesPerRow = 0; - foreach (ExrChannelInfo channelInfo in this.Channels) - { - if (channelInfo.ChannelName.Equals("A") || channelInfo.ChannelName.Equals("R") || channelInfo.ChannelName.Equals("G") || channelInfo.ChannelName.Equals("B") || channelInfo.ChannelName.Equals("Y")) - { - if (channelInfo.PixelType == ExrPixelType.Half) - { - bytesPerRow += 2 * width; - } - else - { - bytesPerRow += 4 * width; - } - } - } + this.ImageDataType = ExrImageDataType.Rgb; + return; + } - return bytesPerRow; + if (hasLuminance && this.Channels.Count == 1) + { + this.ImageDataType = ExrImageDataType.Gray; + return; } - private uint RowsPerBlock() + ExrThrowHelper.ThrowNotSupported("The image contains channels, which are not supported!"); + } + + private bool HasAlpha() + { + foreach (ExrChannelInfo channelInfo in this.Channels) { - switch (this.Compression) + if (channelInfo.ChannelName.Equals("A")) { - case ExrCompressionType.Zip: - case ExrCompressionType.Pxr24: - return 16; - case ExrCompressionType.B44: - case ExrCompressionType.B44A: - case ExrCompressionType.Piz: - return 32; - - default: - return 1; + return true; } } - private ulong ReadUnsignedLong(BufferedReadStream stream) + return false; + } + + private uint CalculateBytesPerRow(uint width) + { + uint bytesPerRow = 0; + foreach (ExrChannelInfo channelInfo in this.Channels) { - int bytesRead = stream.Read(this.buffer, 0, 8); - if (bytesRead != 8) + if (channelInfo.ChannelName.Equals("A") || channelInfo.ChannelName.Equals("R") || channelInfo.ChannelName.Equals("G") || channelInfo.ChannelName.Equals("B") || channelInfo.ChannelName.Equals("Y")) { - ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); + if (channelInfo.PixelType == ExrPixelType.Half) + { + bytesPerRow += 2 * width; + } + else + { + bytesPerRow += 4 * width; + } } - - return BinaryPrimitives.ReadUInt64LittleEndian(this.buffer); } - private uint ReadUnsignedInteger(BufferedReadStream stream) + return bytesPerRow; + } + + private uint RowsPerBlock() + { + switch (this.Compression) { - int bytesRead = stream.Read(this.buffer, 0, 4); - if (bytesRead != 4) - { - ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); - } + case ExrCompressionType.Zip: + case ExrCompressionType.Pxr24: + return 16; + case ExrCompressionType.B44: + case ExrCompressionType.B44A: + case ExrCompressionType.Piz: + return 32; + + default: + return 1; + } + } - return BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + private ulong ReadUnsignedLong(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 8); + if (bytesRead != 8) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); } - private int ReadSignedInteger(BufferedReadStream stream) + return BinaryPrimitives.ReadUInt64LittleEndian(this.buffer); + } + + private uint ReadUnsignedInteger(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 4); + if (bytesRead != 4) { - int bytesRead = stream.Read(this.buffer, 0, 4); - if (bytesRead != 4) - { - ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); - } + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); + } + + return BinaryPrimitives.ReadUInt32LittleEndian(this.buffer); + } - return BinaryPrimitives.ReadInt32LittleEndian(this.buffer); + private int ReadSignedInteger(BufferedReadStream stream) + { + int bytesRead = stream.Read(this.buffer, 0, 4); + if (bytesRead != 4) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); } + + return BinaryPrimitives.ReadInt32LittleEndian(this.buffer); } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderOptions.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderOptions.cs new file mode 100644 index 0000000000..f29e9cfb92 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderOptions.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.OpenExr; + +/// +/// Image decoder options for decoding OpenExr streams. +/// +public sealed class ExrDecoderOptions : ISpecializedDecoderOptions +{ + /// + public DecoderOptions GeneralOptions { get; set; } = new(); +} diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoder.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoder.cs index 71d69596a6..54145afdac 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoder.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoder.cs @@ -1,38 +1,34 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System.IO; -using System.Threading; -using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +/// +/// Image encoder for writing an image to a stream in the OpenExr Format. +/// +public sealed class ExrEncoder : IImageEncoder, IExrEncoderOptions { /// - /// Image encoder for writing an image to a stream in the OpenExr Format. + /// Gets or sets the pixel type of the image. /// - public sealed class ExrEncoder : IImageEncoder, IExrEncoderOptions - { - /// - /// Gets or sets the pixel type of the image. - /// - public ExrPixelType? PixelType { get; set; } + public ExrPixelType? PixelType { get; set; } - /// - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel - { - var encoder = new ExrEncoderCore(this, image.GetMemoryAllocator()); - encoder.Encode(image, stream); - } + /// + public void Encode(Image image, Stream stream) + where TPixel : unmanaged, IPixel + { + ExrEncoderCore encoder = new(this, image.GetMemoryAllocator()); + encoder.Encode(image, stream); + } - /// - public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - var encoder = new ExrEncoderCore(this, image.GetMemoryAllocator()); - return encoder.EncodeAsync(image, stream, cancellationToken); - } + /// + public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + ExrEncoderCore encoder = new(this, image.GetMemoryAllocator()); + return encoder.EncodeAsync(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs index eb626dc1b2..76e186eb8a 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs @@ -1,422 +1,417 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System; using System.Buffers; using System.Buffers.Binary; -using System.Collections.Generic; -using System.IO; using System.Runtime.CompilerServices; -using System.Threading; using SixLabors.ImageSharp.Formats.OpenExr.Compression; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +/// +/// Image encoder for writing an image to a stream in the OpenExr format. +/// +internal sealed class ExrEncoderCore : IImageEncoderInternals { /// - /// Image encoder for writing an image to a stream in the OpenExr format. + /// Reusable buffer. /// - internal sealed class ExrEncoderCore : IImageEncoderInternals - { - /// - /// Reusable buffer. - /// - private readonly byte[] buffer = new byte[8]; - - /// - /// Used for allocating memory during processing operations. - /// - private readonly MemoryAllocator memoryAllocator; - - /// - /// The pixel type of the image. - /// - private ExrPixelType? pixelType; - - /// - /// Initializes a new instance of the class. - /// - /// The encoder options. - /// The memory manager. - public ExrEncoderCore(IExrEncoderOptions options, MemoryAllocator memoryAllocator) - { - this.memoryAllocator = memoryAllocator; - this.pixelType = options.PixelType; - } - - /// - /// Encodes the image to the specified stream from the . - /// - /// The pixel format. - /// The to encode from. - /// The to encode the image data to. - /// The token to request cancellation. - public void Encode(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel - { - Guard.NotNull(image, nameof(image)); - Guard.NotNull(stream, nameof(stream)); + private readonly byte[] buffer = new byte[8]; - Buffer2D pixels = image.Frames.RootFrame.PixelBuffer; - - ImageMetadata metadata = image.Metadata; - ExrMetadata exrMetadata = metadata.GetExrMetadata(); - this.pixelType ??= exrMetadata.PixelType; - int width = image.Width; - int height = image.Height; - var header = new ExrHeaderAttributes() - { - Compression = ExrCompressionType.None, - AspectRatio = 1.0f, - DataWindow = new ExrBox2i(0, 0, width - 1, height - 1), - DisplayWindow = new ExrBox2i(0, 0, width - 1, height - 1), - LineOrder = ExrLineOrder.IncreasingY, - ScreenWindowCenter = new PointF(0.0f, 0.0f), - ScreenWindowWidth = 1, - Channels = new List() - { - 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), - } - }; - - // Write magick bytes. - BinaryPrimitives.WriteInt32LittleEndian(this.buffer, ExrConstants.MagickBytes); - stream.Write(this.buffer.AsSpan(0, 4)); - - // Version number. - this.buffer[0] = 2; + /// + /// Used for allocating memory during processing operations. + /// + private readonly MemoryAllocator memoryAllocator; - // Second, third and fourth bytes store info about the image, set all to default: zero. - this.buffer[1] = 0; - this.buffer[2] = 0; - this.buffer[3] = 0; - stream.Write(this.buffer.AsSpan(0, 4)); + /// + /// The pixel type of the image. + /// + private ExrPixelType? pixelType; - // Write EXR header. - this.WriteHeader(stream, header); + /// + /// Initializes a new instance of the class. + /// + /// The encoder options. + /// The memory manager. + public ExrEncoderCore(IExrEncoderOptions options, MemoryAllocator memoryAllocator) + { + this.memoryAllocator = memoryAllocator; + this.pixelType = options.PixelType; + } - // Write offsets to each pixel row. - int bytesPerChannel = this.pixelType == ExrPixelType.Half ? 2 : 4; - int numberOfChannels = 3; - uint rowSizeBytes = (uint)(width * numberOfChannels * bytesPerChannel); - this.WriteRowOffsets(stream, height, rowSizeBytes); + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); - // Write pixel data. - switch (this.pixelType) - { - case ExrPixelType.Half: - case ExrPixelType.Float: - this.EncodeFloatingPointPixelData(stream, pixels, width, height, rowSizeBytes); - break; - case ExrPixelType.UnsignedInt: - this.EncodeUnsignedIntPixelData(stream, pixels, width, height, rowSizeBytes); - break; - } - } + Buffer2D pixels = image.Frames.RootFrame.PixelBuffer; - private void EncodeFloatingPointPixelData(Stream stream, Buffer2D pixels, int width, int height, uint rowSizeBytes) - where TPixel : unmanaged, IPixel + ImageMetadata metadata = image.Metadata; + ExrMetadata exrMetadata = metadata.GetExrMetadata(); + this.pixelType ??= exrMetadata.PixelType; + int width = image.Width; + int height = image.Height; + var header = new ExrHeaderAttributes() { - using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3); - Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); - Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); - Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); - - for (int y = 0; y < height; y++) + Compression = ExrCompressionType.None, + AspectRatio = 1.0f, + DataWindow = new ExrBox2i(0, 0, width - 1, height - 1), + DisplayWindow = new ExrBox2i(0, 0, width - 1, height - 1), + LineOrder = ExrLineOrder.IncreasingY, + ScreenWindowCenter = new PointF(0.0f, 0.0f), + ScreenWindowWidth = 1, + Channels = new List() { - Span pixelRowSpan = pixels.DangerousGetRowSpan(y); - - for (int x = 0; x < width; x++) - { - var vector4 = pixelRowSpan[x].ToVector4(); - redBuffer[x] = vector4.X; - greenBuffer[x] = vector4.Y; - blueBuffer[x] = vector4.Z; - } - - // Write row index. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); - stream.Write(this.buffer.AsSpan(0, 4)); - - // Write pixel row data size. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, rowSizeBytes); - stream.Write(this.buffer.AsSpan(0, 4)); - - switch (this.pixelType) - { - case ExrPixelType.Float: - this.WriteSingleRow(stream, width, blueBuffer, greenBuffer, redBuffer); - break; - case ExrPixelType.Half: - this.WriteHalfSingleRow(stream, width, blueBuffer, greenBuffer, redBuffer); - break; - } + 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), } - } - - private void EncodeUnsignedIntPixelData(Stream stream, Buffer2D pixels, int width, int height, uint rowSizeBytes) - where TPixel : unmanaged, IPixel - { - using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3); - Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); - Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); - Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); - - var rgb = default(Rgb96); - for (int y = 0; y < height; y++) - { - Span pixelRowSpan = pixels.DangerousGetRowSpan(y); + }; - for (int x = 0; x < width; x++) - { - var vector4 = pixelRowSpan[x].ToVector4(); - rgb.FromVector4(vector4); + // Write magick bytes. + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, ExrConstants.MagickBytes); + stream.Write(this.buffer.AsSpan(0, 4)); - redBuffer[x] = rgb.R; - greenBuffer[x] = rgb.G; - blueBuffer[x] = rgb.B; - } + // Version number. + this.buffer[0] = 2; - // Write row index. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); - stream.Write(this.buffer.AsSpan(0, 4)); + // Second, third and fourth bytes store info about the image, set all to default: zero. + this.buffer[1] = 0; + this.buffer[2] = 0; + this.buffer[3] = 0; + stream.Write(this.buffer.AsSpan(0, 4)); - // Write pixel row data size. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, rowSizeBytes); - stream.Write(this.buffer.AsSpan(0, 4)); + // Write EXR header. + this.WriteHeader(stream, header); - this.WriteUnsignedIntRow(stream, width, blueBuffer, greenBuffer, redBuffer); - } - } + // Write offsets to each pixel row. + int bytesPerChannel = this.pixelType == ExrPixelType.Half ? 2 : 4; + int numberOfChannels = 3; + uint rowSizeBytes = (uint)(width * numberOfChannels * bytesPerChannel); + this.WriteRowOffsets(stream, height, rowSizeBytes); - private void WriteHeader(Stream stream, ExrHeaderAttributes header) + // Write pixel data. + switch (this.pixelType) { - this.WriteChannels(stream, header.Channels); - this.WriteCompression(stream, header.Compression.Value); - this.WriteDataWindow(stream, header.DataWindow.Value); - this.WriteDisplayWindow(stream, header.DisplayWindow.Value); - this.WritePixelAspectRatio(stream, header.AspectRatio.Value); - this.WriteLineOrder(stream, header.LineOrder.Value); - this.WriteScreenWindowCenter(stream, header.ScreenWindowCenter.Value); - this.WriteScreenWindowWidth(stream, header.ScreenWindowWidth.Value); - stream.WriteByte(0); + case ExrPixelType.Half: + case ExrPixelType.Float: + this.EncodeFloatingPointPixelData(stream, pixels, width, height, rowSizeBytes); + break; + case ExrPixelType.UnsignedInt: + this.EncodeUnsignedIntPixelData(stream, pixels, width, height, rowSizeBytes); + break; } + } + + private void EncodeFloatingPointPixelData(Stream stream, Buffer2D pixels, int width, int height, uint rowSizeBytes) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3); + Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); + Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); + Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); - private void WriteSingleRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) - { - this.WriteSingle(stream, blueBuffer[x]); - } + Span pixelRowSpan = pixels.DangerousGetRowSpan(y); for (int x = 0; x < width; x++) { - this.WriteSingle(stream, greenBuffer[x]); + var vector4 = pixelRowSpan[x].ToVector4(); + redBuffer[x] = vector4.X; + greenBuffer[x] = vector4.Y; + blueBuffer[x] = vector4.Z; } - for (int x = 0; x < width; x++) + // Write row index. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); + stream.Write(this.buffer.AsSpan(0, 4)); + + // Write pixel row data size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, rowSizeBytes); + stream.Write(this.buffer.AsSpan(0, 4)); + + switch (this.pixelType) { - this.WriteSingle(stream, redBuffer[x]); + case ExrPixelType.Float: + this.WriteSingleRow(stream, width, blueBuffer, greenBuffer, redBuffer); + break; + case ExrPixelType.Half: + this.WriteHalfSingleRow(stream, width, blueBuffer, greenBuffer, redBuffer); + break; } } + } + + private void EncodeUnsignedIntPixelData(Stream stream, Buffer2D pixels, int width, int height, uint rowSizeBytes) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3); + Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); + Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); + Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); - private void WriteHalfSingleRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + var rgb = default(Rgb96); + for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) - { - this.WriteHalfSingle(stream, blueBuffer[x]); - } + Span pixelRowSpan = pixels.DangerousGetRowSpan(y); for (int x = 0; x < width; x++) { - this.WriteHalfSingle(stream, greenBuffer[x]); - } + var vector4 = pixelRowSpan[x].ToVector4(); + rgb.FromVector4(vector4); - for (int x = 0; x < width; x++) - { - this.WriteHalfSingle(stream, redBuffer[x]); + redBuffer[x] = rgb.R; + greenBuffer[x] = rgb.G; + blueBuffer[x] = rgb.B; } - } - private void WriteUnsignedIntRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) - { - for (int x = 0; x < width; x++) - { - this.WriteUnsignedInt(stream, blueBuffer[x]); - } + // Write row index. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); + stream.Write(this.buffer.AsSpan(0, 4)); - for (int x = 0; x < width; x++) - { - this.WriteUnsignedInt(stream, greenBuffer[x]); - } + // Write pixel row data size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, rowSizeBytes); + stream.Write(this.buffer.AsSpan(0, 4)); - for (int x = 0; x < width; x++) - { - this.WriteUnsignedInt(stream, redBuffer[x]); - } + this.WriteUnsignedIntRow(stream, width, blueBuffer, greenBuffer, redBuffer); } + } - private void WriteRowOffsets(Stream stream, int height, uint rowSizeBytes) + private void WriteHeader(Stream stream, ExrHeaderAttributes header) + { + this.WriteChannels(stream, header.Channels); + this.WriteCompression(stream, header.Compression.Value); + this.WriteDataWindow(stream, header.DataWindow.Value); + this.WriteDisplayWindow(stream, header.DisplayWindow.Value); + this.WritePixelAspectRatio(stream, header.AspectRatio.Value); + this.WriteLineOrder(stream, header.LineOrder.Value); + this.WriteScreenWindowCenter(stream, header.ScreenWindowCenter.Value); + this.WriteScreenWindowWidth(stream, header.ScreenWindowWidth.Value); + stream.WriteByte(0); + } + + private void WriteSingleRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + for (int x = 0; x < width; x++) { - ulong startOfPixelData = (ulong)stream.Position + (8 * (ulong)height); - ulong offset = startOfPixelData; - for (int i = 0; i < height; i++) - { - BinaryPrimitives.WriteUInt64LittleEndian(this.buffer, offset); - stream.Write(this.buffer); - offset += 4 + 4 + rowSizeBytes; - } + this.WriteSingle(stream, blueBuffer[x]); } - private void WriteChannels(Stream stream, IList channels) + for (int x = 0; x < width; x++) { - int attributeSize = 0; - foreach (ExrChannelInfo channelInfo in channels) - { - attributeSize += channelInfo.ChannelName.Length + 1; - attributeSize += 16; - } - - // Last zero byte. - attributeSize++; - this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Channels, ExrConstants.AttibuteTypes.ChannelList, attributeSize); - - foreach (ExrChannelInfo channelInfo in channels) - { - this.WriteChannelInfo(stream, channelInfo); - } + this.WriteSingle(stream, greenBuffer[x]); + } - // Last byte should be zero. - stream.WriteByte(0); + for (int x = 0; x < width; x++) + { + this.WriteSingle(stream, redBuffer[x]); } + } - private void WriteCompression(Stream stream, ExrCompressionType compression) + private void WriteHalfSingleRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + for (int x = 0; x < width; x++) { - this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Compression, ExrConstants.AttibuteTypes.Compression, 1); - stream.WriteByte((byte)compression); + this.WriteHalfSingle(stream, blueBuffer[x]); } - private void WritePixelAspectRatio(Stream stream, float aspectRatio) + for (int x = 0; x < width; x++) { - this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.PixelAspectRatio, ExrConstants.AttibuteTypes.Float, 4); - this.WriteSingle(stream, aspectRatio); + this.WriteHalfSingle(stream, greenBuffer[x]); } - private void WriteLineOrder(Stream stream, ExrLineOrder lineOrder) + for (int x = 0; x < width; x++) { - this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.LineOrder, ExrConstants.AttibuteTypes.LineOrder, 1); - stream.WriteByte((byte)lineOrder); + this.WriteHalfSingle(stream, redBuffer[x]); } + } - private void WriteScreenWindowCenter(Stream stream, PointF screenWindowCenter) + private void WriteUnsignedIntRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + for (int x = 0; x < width; x++) { - this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowCenter, ExrConstants.AttibuteTypes.TwoFloat, 8); - this.WriteSingle(stream, screenWindowCenter.X); - this.WriteSingle(stream, screenWindowCenter.Y); + this.WriteUnsignedInt(stream, blueBuffer[x]); } - private void WriteScreenWindowWidth(Stream stream, float screenWindowWidth) + for (int x = 0; x < width; x++) { - this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowWidth, ExrConstants.AttibuteTypes.Float, 4); - this.WriteSingle(stream, screenWindowWidth); + this.WriteUnsignedInt(stream, greenBuffer[x]); } - private void WriteDataWindow(Stream stream, ExrBox2i dataWindow) + for (int x = 0; x < width; x++) { - this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DataWindow, ExrConstants.AttibuteTypes.BoxInt, 16); - this.WriteBoxInteger(stream, dataWindow); + this.WriteUnsignedInt(stream, redBuffer[x]); } + } - private void WriteDisplayWindow(Stream stream, ExrBox2i displayWindow) + private void WriteRowOffsets(Stream stream, int height, uint rowSizeBytes) + { + ulong startOfPixelData = (ulong)stream.Position + (8 * (ulong)height); + ulong offset = startOfPixelData; + for (int i = 0; i < height; i++) { - this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DisplayWindow, ExrConstants.AttibuteTypes.BoxInt, 16); - this.WriteBoxInteger(stream, displayWindow); + BinaryPrimitives.WriteUInt64LittleEndian(this.buffer, offset); + stream.Write(this.buffer); + offset += 4 + 4 + rowSizeBytes; } + } - private void WriteAttributeInformation(Stream stream, string name, string type, int size) + private void WriteChannels(Stream stream, IList channels) + { + int attributeSize = 0; + foreach (ExrChannelInfo channelInfo in channels) { - // Write attribute name. - this.WriteString(stream, name); + attributeSize += channelInfo.ChannelName.Length + 1; + attributeSize += 16; + } - // Write attribute type. - this.WriteString(stream, type); + // Last zero byte. + attributeSize++; + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Channels, ExrConstants.AttibuteTypes.ChannelList, attributeSize); - // Write attribute size. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)size); - stream.Write(this.buffer.AsSpan(0, 4)); + foreach (ExrChannelInfo channelInfo in channels) + { + this.WriteChannelInfo(stream, channelInfo); } - private void WriteChannelInfo(Stream stream, ExrChannelInfo channelInfo) - { - this.WriteString(stream, channelInfo.ChannelName); + // Last byte should be zero. + stream.WriteByte(0); + } - BinaryPrimitives.WriteInt32LittleEndian(this.buffer, (int)channelInfo.PixelType); - stream.Write(this.buffer.AsSpan(0, 4)); + private void WriteCompression(Stream stream, ExrCompressionType compression) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Compression, ExrConstants.AttibuteTypes.Compression, 1); + stream.WriteByte((byte)compression); + } - stream.WriteByte(channelInfo.PLinear); + private void WritePixelAspectRatio(Stream stream, float aspectRatio) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.PixelAspectRatio, ExrConstants.AttibuteTypes.Float, 4); + this.WriteSingle(stream, aspectRatio); + } - // Next 3 bytes are reserved and will set to zero. - stream.WriteByte(0); - stream.WriteByte(0); - stream.WriteByte(0); + private void WriteLineOrder(Stream stream, ExrLineOrder lineOrder) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.LineOrder, ExrConstants.AttibuteTypes.LineOrder, 1); + stream.WriteByte((byte)lineOrder); + } - BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.XSampling); - stream.Write(this.buffer.AsSpan(0, 4)); + private void WriteScreenWindowCenter(Stream stream, PointF screenWindowCenter) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowCenter, ExrConstants.AttibuteTypes.TwoFloat, 8); + this.WriteSingle(stream, screenWindowCenter.X); + this.WriteSingle(stream, screenWindowCenter.Y); + } - BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.YSampling); - stream.Write(this.buffer.AsSpan(0, 4)); - } + private void WriteScreenWindowWidth(Stream stream, float screenWindowWidth) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.ScreenWindowWidth, ExrConstants.AttibuteTypes.Float, 4); + this.WriteSingle(stream, screenWindowWidth); + } - private void WriteString(Stream stream, string str) - { - foreach (char c in str) - { - stream.WriteByte((byte)c); - } + private void WriteDataWindow(Stream stream, ExrBox2i dataWindow) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DataWindow, ExrConstants.AttibuteTypes.BoxInt, 16); + this.WriteBoxInteger(stream, dataWindow); + } - // Write termination byte. - stream.WriteByte(0); - } + private void WriteDisplayWindow(Stream stream, ExrBox2i displayWindow) + { + this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.DisplayWindow, ExrConstants.AttibuteTypes.BoxInt, 16); + this.WriteBoxInteger(stream, displayWindow); + } - private void WriteBoxInteger(Stream stream, ExrBox2i box) - { - BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMin); - stream.Write(this.buffer.AsSpan(0, 4)); + private void WriteAttributeInformation(Stream stream, string name, string type, int size) + { + // Write attribute name. + this.WriteString(stream, name); - BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMin); - stream.Write(this.buffer.AsSpan(0, 4)); + // Write attribute type. + this.WriteString(stream, type); - BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMax); - stream.Write(this.buffer.AsSpan(0, 4)); + // Write attribute size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)size); + stream.Write(this.buffer.AsSpan(0, 4)); + } - BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMax); - stream.Write(this.buffer.AsSpan(0, 4)); - } + private void WriteChannelInfo(Stream stream, ExrChannelInfo channelInfo) + { + this.WriteString(stream, channelInfo.ChannelName); - [MethodImpl(InliningOptions.ShortMethod)] - private unsafe void WriteSingle(Stream stream, float value) - { - BinaryPrimitives.WriteInt32LittleEndian(this.buffer, *(int*)&value); - stream.Write(this.buffer.AsSpan(0, 4)); - } + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, (int)channelInfo.PixelType); + stream.Write(this.buffer.AsSpan(0, 4)); - [MethodImpl(InliningOptions.ShortMethod)] - private void WriteHalfSingle(Stream stream, float value) - { - ushort valueAsShort = HalfTypeHelper.Pack(value); - BinaryPrimitives.WriteUInt16LittleEndian(this.buffer, valueAsShort); - stream.Write(this.buffer.AsSpan(0, 2)); - } + stream.WriteByte(channelInfo.PLinear); + + // Next 3 bytes are reserved and will set to zero. + stream.WriteByte(0); + stream.WriteByte(0); + stream.WriteByte(0); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.XSampling); + stream.Write(this.buffer.AsSpan(0, 4)); - [MethodImpl(InliningOptions.ShortMethod)] - private void WriteUnsignedInt(Stream stream, uint value) + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, channelInfo.YSampling); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + private void WriteString(Stream stream, string str) + { + foreach (char c in str) { - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, value); - stream.Write(this.buffer.AsSpan(0, 4)); + stream.WriteByte((byte)c); } + + // Write termination byte. + stream.WriteByte(0); + } + + private void WriteBoxInteger(Stream stream, ExrBox2i box) + { + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMin); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMin); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.XMax); + stream.Write(this.buffer.AsSpan(0, 4)); + + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, box.YMax); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private unsafe void WriteSingle(Stream stream, float value) + { + BinaryPrimitives.WriteInt32LittleEndian(this.buffer, *(int*)&value); + stream.Write(this.buffer.AsSpan(0, 4)); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private void WriteHalfSingle(Stream stream, float value) + { + ushort valueAsShort = HalfTypeHelper.Pack(value); + BinaryPrimitives.WriteUInt16LittleEndian(this.buffer, valueAsShort); + stream.Write(this.buffer.AsSpan(0, 2)); + } + + [MethodImpl(InliningOptions.ShortMethod)] + private void WriteUnsignedInt(Stream stream, uint value) + { + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, value); + stream.Write(this.buffer.AsSpan(0, 4)); } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrFormat.cs b/src/ImageSharp/Formats/OpenExr/ExrFormat.cs index 2cbce0970c..d367977fc6 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrFormat.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrFormat.cs @@ -1,38 +1,34 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System.Collections.Generic; -using SixLabors.ImageSharp.Formats.Bmp; +namespace SixLabors.ImageSharp.Formats.OpenExr; -namespace SixLabors.ImageSharp.Formats.OpenExr +/// +/// Registers the image encoders, decoders and mime type detectors for the OpenExr format. +/// +public sealed class ExrFormat : IImageFormat { - /// - /// Registers the image encoders, decoders and mime type detectors for the OpenExr format. - /// - public sealed class ExrFormat : IImageFormat + private ExrFormat() { - private ExrFormat() - { - } + } - /// - /// Gets the current instance. - /// - public static ExrFormat Instance { get; } = new(); + /// + /// Gets the current instance. + /// + public static ExrFormat Instance { get; } = new(); - /// - public string Name => "EXR"; + /// + public string Name => "EXR"; - /// - public string DefaultMimeType => "image/x-exr"; + /// + public string DefaultMimeType => "image/x-exr"; - /// - public IEnumerable MimeTypes => ExrConstants.MimeTypes; + /// + public IEnumerable MimeTypes => ExrConstants.MimeTypes; - /// - public IEnumerable FileExtensions => ExrConstants.FileExtensions; + /// + public IEnumerable FileExtensions => ExrConstants.FileExtensions; - /// - public ExrMetadata CreateDefaultFormatMetadata() => new(); - } + /// + public ExrMetadata CreateDefaultFormatMetadata() => new(); } diff --git a/src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs b/src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs index 76d14311a5..b3b3cdb604 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs @@ -1,78 +1,76 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System.Collections.Generic; using SixLabors.ImageSharp.Formats.OpenExr.Compression; -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +internal class ExrHeaderAttributes { - internal class ExrHeaderAttributes - { - public IList Channels { get; set; } + public IList Channels { get; set; } - public ExrCompressionType? Compression { get; set; } + public ExrCompressionType? Compression { get; set; } - public ExrBox2i? DataWindow { get; set; } + public ExrBox2i? DataWindow { get; set; } - public ExrBox2i? DisplayWindow { get; set; } + public ExrBox2i? DisplayWindow { get; set; } - public ExrLineOrder? LineOrder { get; set; } + public ExrLineOrder? LineOrder { get; set; } - public float? AspectRatio { get; set; } + public float? AspectRatio { get; set; } - public float? ScreenWindowWidth { get; set; } + public float? ScreenWindowWidth { get; set; } - public PointF? ScreenWindowCenter { get; set; } + public PointF? ScreenWindowCenter { get; set; } - public uint? TileXSize { get; set; } + public uint? TileXSize { get; set; } - public uint? TileYSize { get; set; } + public uint? TileYSize { get; set; } - public int? ChunkCount { get; set; } + public int? ChunkCount { get; set; } - public bool IsValid() + public bool IsValid() + { + if (!this.Compression.HasValue) { - 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; + 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/ExrImageDataType.cs b/src/ImageSharp/Formats/OpenExr/ExrImageDataType.cs index d17c3ad61d..4c3e19af81 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrImageDataType.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrImageDataType.cs @@ -1,16 +1,15 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +internal enum ExrImageDataType { - internal enum ExrImageDataType - { - Unknown = 0, + Unknown = 0, - Rgb = 1, + Rgb = 1, - Rgba = 2, + Rgba = 2, - Gray = 3, - } + Gray = 3, } diff --git a/src/ImageSharp/Formats/OpenExr/ExrImageFormatDetector.cs b/src/ImageSharp/Formats/OpenExr/ExrImageFormatDetector.cs index 27230306db..c14ab947a9 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrImageFormatDetector.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrImageFormatDetector.cs @@ -1,31 +1,29 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System; using System.Buffers.Binary; -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +/// +/// Detects OpenExr file headers. +/// +public sealed class ExrImageFormatDetector : IImageFormatDetector { - /// - /// Detects OpenExr file headers. - /// - public sealed class ExrImageFormatDetector : IImageFormatDetector - { - /// - public int HeaderSize => 4; + /// + public int HeaderSize => 4; - /// - public IImageFormat DetectFormat(ReadOnlySpan header) => this.IsSupportedFileFormat(header) ? ExrFormat.Instance : null; + /// + public IImageFormat DetectFormat(ReadOnlySpan header) => this.IsSupportedFileFormat(header) ? ExrFormat.Instance : null; - private bool IsSupportedFileFormat(ReadOnlySpan header) + private bool IsSupportedFileFormat(ReadOnlySpan header) + { + if (header.Length >= this.HeaderSize) { - if (header.Length >= this.HeaderSize) - { - int fileTypeMarker = BinaryPrimitives.ReadInt32LittleEndian(header); - return fileTypeMarker == ExrConstants.MagickBytes; - } - - return false; + int fileTypeMarker = BinaryPrimitives.ReadInt32LittleEndian(header); + return fileTypeMarker == ExrConstants.MagickBytes; } + + return false; } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrImageType.cs b/src/ImageSharp/Formats/OpenExr/ExrImageType.cs index e18ee363f2..a59aadbd62 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrImageType.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrImageType.cs @@ -1,12 +1,11 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +internal enum ExrImageType { - internal enum ExrImageType - { - ScanLine = 0, + ScanLine = 0, - Tiled = 1 - } + Tiled = 1 } diff --git a/src/ImageSharp/Formats/OpenExr/ExrLineOrder.cs b/src/ImageSharp/Formats/OpenExr/ExrLineOrder.cs index 1005414f22..dcf1c37c9e 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrLineOrder.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrLineOrder.cs @@ -1,14 +1,13 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +internal enum ExrLineOrder : byte { - internal enum ExrLineOrder : byte - { - IncreasingY = 0, + IncreasingY = 0, - DecreasingY = 1, + DecreasingY = 1, - RandomY = 2 - } + RandomY = 2 } diff --git a/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs b/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs index 7af01a38f0..8b090d6842 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs @@ -1,32 +1,31 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +/// +/// Provides OpenExr specific metadata information for the image. +/// +public class ExrMetadata : IDeepCloneable { /// - /// Provides OpenExr specific metadata information for the image. + /// Initializes a new instance of the class. /// - public class ExrMetadata : IDeepCloneable + public ExrMetadata() { - /// - /// 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) => this.PixelType = other.PixelType; + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private ExrMetadata(ExrMetadata other) => this.PixelType = other.PixelType; - /// - /// Gets or sets the pixel format. - /// - public ExrPixelType PixelType { get; set; } = ExrPixelType.Float; + /// + /// Gets or sets the pixel format. + /// + public ExrPixelType PixelType { get; set; } = ExrPixelType.Float; - /// - public IDeepCloneable DeepClone() => new ExrMetadata(this); - } + /// + public IDeepCloneable DeepClone() => new ExrMetadata(this); } diff --git a/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs b/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs index 2a57afe988..0dd49eaf8f 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs @@ -1,26 +1,25 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +/// +/// The different pixel formats for a OpenEXR image. +/// +public enum ExrPixelType { /// - /// The different pixel formats for a OpenEXR image. + /// unsigned int (32 bit). /// - public enum ExrPixelType - { - /// - /// unsigned int (32 bit). - /// - UnsignedInt = 0, + UnsignedInt = 0, - /// - /// half (16 bit floating point). - /// - Half = 1, + /// + /// half (16 bit floating point). + /// + Half = 1, - /// - /// float (32 bit floating point). - /// - Float = 2 - } + /// + /// float (32 bit floating point). + /// + Float = 2 } diff --git a/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs b/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs index b68fb5a2f4..62f9f1b2f4 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs @@ -1,32 +1,30 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System; using System.Runtime.CompilerServices; -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +/// +/// Cold path optimizations for throwing exr format based exceptions. +/// +internal static class ExrThrowHelper { - /// - /// Cold path optimizations for throwing exr format based exceptions. - /// - internal static class ExrThrowHelper - { - [MethodImpl(InliningOptions.ColdPath)] - public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}"); + [MethodImpl(InliningOptions.ColdPath)] + public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}"); - [MethodImpl(MethodImplOptions.NoInlining)] - public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); + [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"); + [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 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() => throw new InvalidImageContentException("Invalid EXR image header"); - [MethodImpl(InliningOptions.ColdPath)] - public static void ThrowInvalidImageHeader(string msg) => throw new InvalidImageContentException(msg); - } + [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 deleted file mode 100644 index e61ce85378..0000000000 --- a/src/ImageSharp/Formats/OpenExr/IExrDecoderOptions.cs +++ /dev/null @@ -1,12 +0,0 @@ -// 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/IExrEncoderOptions.cs b/src/ImageSharp/Formats/OpenExr/IExrEncoderOptions.cs index 938236b692..ba2db62a00 100644 --- a/src/ImageSharp/Formats/OpenExr/IExrEncoderOptions.cs +++ b/src/ImageSharp/Formats/OpenExr/IExrEncoderOptions.cs @@ -1,16 +1,15 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr +namespace SixLabors.ImageSharp.Formats.OpenExr; + +/// +/// Configuration options for use during OpenExr encoding. +/// +internal interface IExrEncoderOptions { /// - /// Configuration options for use during OpenExr encoding. + /// Gets the pixel type of the image. /// - internal interface IExrEncoderOptions - { - /// - /// Gets the pixel type of the image. - /// - ExrPixelType? PixelType { get; } - } + ExrPixelType? PixelType { get; } } diff --git a/src/ImageSharp/Formats/OpenExr/MetadataExtensions.cs b/src/ImageSharp/Formats/OpenExr/MetadataExtensions.cs index 24508f06a3..2af0a57b07 100644 --- a/src/ImageSharp/Formats/OpenExr/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/OpenExr/MetadataExtensions.cs @@ -1,21 +1,20 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.OpenExr; using SixLabors.ImageSharp.Metadata; -namespace SixLabors.ImageSharp +namespace SixLabors.ImageSharp; + +/// +/// Extension methods for the type. +/// +public static partial class MetadataExtensions { /// - /// Extension methods for the type. + /// Gets the open exr format specific metadata for the image. /// - public static partial class MetadataExtensions - { - /// - /// Gets the open exr format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static ExrMetadata GetExrMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(ExrFormat.Instance); - } + /// The metadata this method extends. + /// The . + public static ExrMetadata GetExrMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(ExrFormat.Instance); } diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs index 7e233c9655..b6d7f6b3f4 100644 --- a/src/ImageSharp/IO/BufferedReadStream.cs +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Buffers.Binary; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.IO; @@ -153,6 +154,31 @@ public override int ReadByte() } } + /// + /// Reads a float value. + /// + /// The value. + public float ReadSingle(byte[] data) + { + int offset = 0; + int bytesToRead = 4; + while (bytesToRead > 0) + { + int bytesRead = this.Read(data, offset, bytesToRead); + if (bytesRead == 0) + { + throw new ImageFormatException("Not enough data to read a float value from the stream"); + } + + bytesToRead -= bytesRead; + offset += bytesRead; + } + + int intValue = BinaryPrimitives.ReadInt32BigEndian(data); + + return Unsafe.As(ref intValue); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override int Read(byte[] buffer, int offset, int count) diff --git a/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs index 9ff506efad..323000bc59 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs @@ -1,156 +1,67 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.IO; -using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.OpenExr; using SixLabors.ImageSharp.PixelFormats; -using Xunit; -namespace SixLabors.ImageSharp.Tests.Formats.Exr +namespace SixLabors.ImageSharp.Tests.Formats.Exr; + +[Trait("Format", "Exr")] +public class ImageExtensionsTest { - [Trait("Format", "Exr")] - public class ImageExtensionsTest + [Fact] + public void SaveAsExr_Stream() { - [Fact] - public void SaveAsExr_Path() - { - string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); - string file = Path.Combine(dir, "SaveAsExr_Path.exr"); - - using (var image = new Image(10, 10)) - { - image.SaveAsOpenExr(file); - } - - using (Image.Load(file, out IImageFormat mime)) - { - Assert.Equal("image/x-exr", mime.DefaultMimeType); - } - } + using var memoryStream = new MemoryStream(); - [Fact] - public async Task SaveAsExrAsync_Path() + using (var image = new Image(10, 10)) { - string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); - string file = Path.Combine(dir, "SaveAsExrAsync_Path.exr"); - - using (var image = new Image(10, 10)) - { - await image.SaveAsOpenExrAsync(file); - } - - using (Image.Load(file, out IImageFormat mime)) - { - Assert.Equal("image/x-exr", mime.DefaultMimeType); - } + image.SaveAsOpenExr(memoryStream, new ExrEncoder()); } - [Fact] - public void SaveAsExr_Path_Encoder() - { - string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); - string file = Path.Combine(dir, "SaveAsExr_Path_Encoder.exr"); - - using (var image = new Image(10, 10)) - { - image.SaveAsOpenExr(file, new ExrEncoder()); - } + memoryStream.Position = 0; - using (Image.Load(file, out IImageFormat mime)) - { - Assert.Equal("image/x-exr", mime.DefaultMimeType); - } - } - - [Fact] - public async Task SaveAsExrAsync_Path_Encoder() + using (Image.Load(memoryStream, out IImageFormat mime)) { - string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); - string file = Path.Combine(dir, "SaveAsExrAsync_Path_Encoder.tiff"); - - using (var image = new Image(10, 10)) - { - await image.SaveAsOpenExrAsync(file, new ExrEncoder()); - } - - using (Image.Load(file, out IImageFormat mime)) - { - Assert.Equal("image/x-exr", mime.DefaultMimeType); - } + Assert.Equal("image/x-exr", mime.DefaultMimeType); } + } - [Fact] - public void SaveAsExr_Stream() - { - using var memoryStream = new MemoryStream(); - - using (var image = new Image(10, 10)) - { - image.SaveAsOpenExr(memoryStream); - } - - memoryStream.Position = 0; - - using (Image.Load(memoryStream, out IImageFormat mime)) - { - Assert.Equal("image/x-exr", mime.DefaultMimeType); - } - } + [Fact] + public void SaveAsExr_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); - [Fact] - public async Task SaveAsExrAsync_StreamAsync() + using (var image = new Image(10, 10)) { - using var memoryStream = new MemoryStream(); - - using (var image = new Image(10, 10)) - { - await image.SaveAsOpenExrAsync(memoryStream); - } - - memoryStream.Position = 0; - - using (Image.Load(memoryStream, out IImageFormat mime)) - { - Assert.Equal("image/x-exr", mime.DefaultMimeType); - } + image.SaveAsOpenExr(memoryStream, new ExrEncoder()); } - [Fact] - public void SaveAsExr_Stream_Encoder() - { - using var memoryStream = new MemoryStream(); - - using (var image = new Image(10, 10)) - { - image.SaveAsOpenExr(memoryStream, new ExrEncoder()); - } - - memoryStream.Position = 0; + memoryStream.Position = 0; - using (Image.Load(memoryStream, out IImageFormat mime)) - { - Assert.Equal("image/x-exr", mime.DefaultMimeType); - } + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/x-exr", mime.DefaultMimeType); } + } - [Fact] - public async Task SaveAsExrAsync_Stream_Encoder() - { - using var memoryStream = new MemoryStream(); + [Fact] + public async Task SaveAsExrAsync_Stream_Encoder() + { + using var memoryStream = new MemoryStream(); - using (var image = new Image(10, 10)) - { - await image.SaveAsOpenExrAsync(memoryStream, new ExrEncoder()); - } + using (var image = new Image(10, 10)) + { + await image.SaveAsOpenExrAsync(memoryStream, new ExrEncoder()); + } - memoryStream.Position = 0; + memoryStream.Position = 0; - using (Image.Load(memoryStream, out IImageFormat mime)) - { - Assert.Equal("image/x-exr", mime.DefaultMimeType); - } + using (Image.Load(memoryStream, out IImageFormat mime)) + { + Assert.Equal("image/x-exr", mime.DefaultMimeType); } } } + diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index 0fa7f1684a..fa5e4bdeff 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -144,11 +144,6 @@ public void ImageCanConvertFormat() { image.SaveAsTiff(output); } - - using (FileStream output = File.OpenWrite(Path.Combine(path, $"{file.FileNameWithoutExtension}.exr"))) - { - image.SaveAsOpenExr(output); - } } } From ce21f1afaa22e53e8d5686f483f854f713a263b3 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 20 Sep 2022 17:25:04 +0200 Subject: [PATCH 16/66] Add exr decoder tests --- src/ImageSharp/Configuration.cs | 4 +- .../Formats/Exr/ExrDecoderTests.cs | 54 +++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 8 +++ .../TestUtilities/TestEnvironment.Formats.cs | 6 ++- tests/Images/Input/Exr/Calliphora_rle.exr | 3 ++ .../Input/Exr/Calliphora_uncompressed.exr | 3 ++ tests/Images/Input/Exr/Calliphora_zip.exr | 3 ++ tests/Images/Input/Exr/Calliphora_zips.exr | 3 ++ 8 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs create mode 100644 tests/Images/Input/Exr/Calliphora_rle.exr create mode 100644 tests/Images/Input/Exr/Calliphora_uncompressed.exr create mode 100644 tests/Images/Input/Exr/Calliphora_zip.exr create mode 100644 tests/Images/Input/Exr/Calliphora_zips.exr diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 474e91a43b..f00cc0eaa0 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -213,6 +213,7 @@ public void Configure(IConfigurationModule configuration) /// . /// . /// . + /// . /// /// The default configuration of . internal static Configuration CreateDefaultInstance() => new( @@ -223,5 +224,6 @@ public void Configure(IConfigurationModule configuration) new PbmConfigurationModule(), new TgaConfigurationModule(), new TiffConfigurationModule(), - new WebpConfigurationModule()); + new WebpConfigurationModule(), + new ExrConfigurationModule()); } diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs new file mode 100644 index 0000000000..709a442630 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.OpenExr; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.Formats.Exr; + +[Trait("Format", "Exr")] +[ValidateDisposedMemoryAllocations] +public class ExrDecoderTests +{ + private static ExrDecoder ExrDecoder => new(); + + [Theory] + [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Uncompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider); + } + + [Theory] + [WithFile(TestImages.Exr.Zip, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_ZipCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider); + } + + [Theory] + [WithFile(TestImages.Exr.Zips, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_ZipsCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider); + } + + [Theory] + [WithFile(TestImages.Exr.Rle, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_RunLengthCompressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider); + } +} diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 676d460e56..8922e3ebee 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -998,4 +998,12 @@ public static class Pbm public const string RgbPlainNormalized = "Pbm/rgb_plain_normalized.ppm"; public const string RgbPlainMagick = "Pbm/rgb_plain_magick.ppm"; } + + public static class Exr + { + public const string Uncompressed = "Exr/Calliphora_uncompressed.exr"; + public const string Zip = "Exr/Calliphora_zip.exr"; + public const string Zips = "Exr/Calliphora_zips.exr"; + public const string Rle = "Exr/Calliphora_rle.exr"; + } } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs index 89b43a0661..758d689025 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -5,6 +5,7 @@ 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; @@ -59,12 +60,13 @@ private static Configuration CreateDefaultConfiguration() new PbmConfigurationModule(), new TgaConfigurationModule(), new WebpConfigurationModule(), - new TiffConfigurationModule()); + new TiffConfigurationModule(), + new ExrConfigurationModule()); IImageEncoder pngEncoder = IsWindows ? SystemDrawingReferenceEncoder.Png : new ImageSharpPngEncoderWithDefaultConfiguration(); IImageEncoder bmpEncoder = IsWindows ? SystemDrawingReferenceEncoder.Bmp : new BmpEncoder(); - // Magick codecs should work on all platforms + // Magick codecs should work on all platforms. cfg.ConfigureCodecs( PngFormat.Instance, MagickReferenceDecoder.Instance, diff --git a/tests/Images/Input/Exr/Calliphora_rle.exr b/tests/Images/Input/Exr/Calliphora_rle.exr new file mode 100644 index 0000000000..20878f8896 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_rle.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6302e72676f5574b8a9dde7e4385cff1c115e9833b630118328b88aea07e31d0 +size 4098454 diff --git a/tests/Images/Input/Exr/Calliphora_uncompressed.exr b/tests/Images/Input/Exr/Calliphora_uncompressed.exr new file mode 100644 index 0000000000..f0003a1f91 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_uncompressed.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71613ff5972fa9e7b405ac944d159d1791ad229f5560da616438c9d718eafd24 +size 5798633 diff --git a/tests/Images/Input/Exr/Calliphora_zip.exr b/tests/Images/Input/Exr/Calliphora_zip.exr new file mode 100644 index 0000000000..5d17150a49 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_zip.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92c78a7cf9fce29e725511acadf49f0f784914da5bef55b0e0c4e75ba09c3a75 +size 2652397 diff --git a/tests/Images/Input/Exr/Calliphora_zips.exr b/tests/Images/Input/Exr/Calliphora_zips.exr new file mode 100644 index 0000000000..b4b2976822 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_zips.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e34a9b0bb7fd6eb24eef633dea737653812fd8ab5c5352f6e10523055823b1d +size 2918571 From a9620ee52fb319ac6c439038555460d87485b4f3 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 23 Sep 2022 19:35:37 +0200 Subject: [PATCH 17/66] Add support for decoding b44 compressed exr files --- .../Decompressors/B44Compression.cs | 191 ++++++++++++++++++ .../NoneExrCompression.cs | 2 +- .../RunLengthCompression.cs | 16 +- .../ZipExrCompression.cs | 8 +- .../Compression/ExrDecompressorFactory.cs | 6 +- .../Formats/OpenExr/ExrDecoderCore.cs | 39 +++- .../Formats/OpenExr/ExrEncoderCore.cs | 13 +- .../Formats/OpenExr/ExrThrowHelper.cs | 8 - .../Formats/Exr/ExrDecoderTests.cs | 10 + tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Exr/Calliphora_b44.exr | 3 + 11 files changed, 256 insertions(+), 41 deletions(-) create mode 100644 src/ImageSharp/Formats/OpenExr/Compression/Decompressors/B44Compression.cs rename src/ImageSharp/Formats/OpenExr/Compression/{Compressors => Decompressors}/NoneExrCompression.cs (89%) rename src/ImageSharp/Formats/OpenExr/Compression/{Compressors => Decompressors}/RunLengthCompression.cs (82%) rename src/ImageSharp/Formats/OpenExr/Compression/{Compressors => Decompressors}/ZipExrCompression.cs (86%) create mode 100644 tests/Images/Input/Exr/Calliphora_b44.exr diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/B44Compression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/B44Compression.cs new file mode 100644 index 0000000000..55e9003bc1 --- /dev/null +++ b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/B44Compression.cs @@ -0,0 +1,191 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; + +internal class B44Compression : ExrBaseDecompressor +{ + private readonly int width; + + private readonly int height; + + private readonly uint rowsPerBlock; + + private readonly int channelCount; + + private byte[] scratch = new byte[14]; + + private ushort[] s = new ushort[16]; + + private IMemoryOwner tmpBuffer; + + public B44Compression(MemoryAllocator allocator, uint uncompressedBytes, int width, int height, uint rowsPerBlock, int channelCount) + : base(allocator, uncompressedBytes) + { + this.width = width; + this.height = height; + this.rowsPerBlock = rowsPerBlock; + this.channelCount = channelCount; + this.tmpBuffer = allocator.Allocate((int)(width * rowsPerBlock * channelCount)); + } + + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + Span outputBuffer = MemoryMarshal.Cast(buffer); + Span decompressed = this.tmpBuffer.GetSpan(); + int outputOffset = 0; + int bytesLeft = (int)compressedBytes; + for (int i = 0; i < this.channelCount && bytesLeft > 0; i++) + { + for (int y = 0; y < this.rowsPerBlock; y += 4) + { + Span row0 = decompressed.Slice(outputOffset, this.width); + outputOffset += this.width; + Span row1 = decompressed.Slice(outputOffset, this.width); + outputOffset += this.width; + Span row2 = decompressed.Slice(outputOffset, this.width); + outputOffset += this.width; + Span row3 = decompressed.Slice(outputOffset, this.width); + outputOffset += this.width; + + int rowOffset = 0; + for (int x = 0; x < this.width && bytesLeft > 0; x += 4) + { + int bytesRead = stream.Read(this.scratch, 0, 3); + if (bytesRead == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream"); + } + + if (this.scratch[2] >= 13 << 2) + { + Unpack3(this.scratch, this.s); + bytesLeft -= 3; + } + else + { + bytesRead = stream.Read(this.scratch, 3, 11); + if (bytesRead == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream"); + } + + Unpack14(this.scratch, this.s); + bytesLeft -= 14; + } + + int n = x + 3 < this.width ? 4 : this.width - x; + if (y + 3 < this.rowsPerBlock) + { + this.s.AsSpan(0, n).CopyTo(row0.Slice(rowOffset)); + this.s.AsSpan(4, n).CopyTo(row1.Slice(rowOffset)); + this.s.AsSpan(8, n).CopyTo(row2.Slice(rowOffset)); + this.s.AsSpan(12, n).CopyTo(row3.Slice(rowOffset)); + } + else + { + this.s.AsSpan(0, n).CopyTo(row0.Slice(rowOffset)); + if (y + 1 < this.rowsPerBlock) + { + this.s.AsSpan(4, n).CopyTo(row1.Slice(rowOffset)); + } + + if (y + 2 < this.rowsPerBlock) + { + this.s.AsSpan(8, n).CopyTo(row2.Slice(rowOffset)); + } + } + + rowOffset += 4; + } + + if (bytesLeft <= 0) + { + break; + } + } + } + + // Rearrange the decompressed data such that the data for each scan line form a contiguous block. + int offsetDecompressed = 0; + int offsetOutput = 0; + int blockSize = (int)(this.width * this.rowsPerBlock); + for (int y = 0; y < this.rowsPerBlock; y++) + { + for (int i = 0; i < this.channelCount; i++) + { + decompressed.Slice(offsetDecompressed + (i * blockSize), this.width).CopyTo(outputBuffer.Slice(offsetOutput)); + offsetOutput += this.width; + } + + offsetDecompressed += this.width; + } + } + + // Unpack a 14-byte block into 4 by 4 16-bit pixels. + private static void Unpack14(Span b, Span s) + { + s[0] = (ushort)((b[0] << 8) | b[1]); + + ushort shift = (ushort)(b[2] >> 2); + ushort bias = (ushort)(0x20u << shift); + + s[4] = (ushort)(s[0] + ((((b[2] << 4) | (b[3] >> 4)) & 0x3fu) << shift) - bias); + s[8] = (ushort)(s[4] + ((((b[3] << 2) | (b[4] >> 6)) & 0x3fu) << shift) - bias); + s[12] = (ushort)(s[8] + ((b[4] & 0x3fu) << shift) - bias); + + s[1] = (ushort)(s[0] + ((uint)(b[5] >> 2) << shift) - bias); + s[5] = (ushort)(s[4] + ((((b[5] << 4) | (b[6] >> 4)) & 0x3fu) << shift) - bias); + s[9] = (ushort)(s[8] + ((((b[6] << 2) | (b[7] >> 6)) & 0x3fu) << shift) - bias); + s[13] = (ushort)(s[12] + ((b[7] & 0x3fu) << shift) - bias); + + s[2] = (ushort)(s[1] + ((uint)(b[8] >> 2) << shift) - bias); + s[6] = (ushort)(s[5] + ((((b[8] << 4) | (b[9] >> 4)) & 0x3fu) << shift) - bias); + s[10] = (ushort)(s[9] + ((((b[9] << 2) | (b[10] >> 6)) & 0x3fu) << shift) - bias); + s[14] = (ushort)(s[13] + ((b[10] & 0x3fu) << shift) - bias); + + s[3] = (ushort)(s[2] + ((uint)(b[11] >> 2) << shift) - bias); + s[7] = (ushort)(s[6] + ((((b[11] << 4) | (b[12] >> 4)) & 0x3fu) << shift) - bias); + s[11] = (ushort)(s[10] + ((((b[12] << 2) | (b[13] >> 6)) & 0x3fu) << shift) - bias); + s[15] = (ushort)(s[14] + ((b[13] & 0x3fu) << shift) - bias); + + for (int i = 0; i < 16; ++i) + { + if ((s[i] & 0x8000) != 0) + { + s[i] &= 0x7fff; + } + else + { + s[i] = (ushort)~s[i]; + } + } + } + + // Unpack a 3-byte block into 4 by 4 identical 16-bit pixels. + private static void Unpack3(Span b, Span s) + { + s[0] = (ushort)((b[0] << 8) | b[1]); + + if ((s[0] & 0x8000) != 0) + { + s[0] &= 0x7fff; + } + else + { + s[0] = (ushort)~s[0]; + } + + for (int i = 1; i < 16; ++i) + { + s[i] = s[0]; + } + } + + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); +} diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/NoneExrCompression.cs similarity index 89% rename from src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs rename to src/ImageSharp/Formats/OpenExr/Compression/Decompressors/NoneExrCompression.cs index 333c453ebd..f5f16a0fd4 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/NoneExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/NoneExrCompression.cs @@ -4,7 +4,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; internal class NoneExrCompression : ExrBaseDecompressor { diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/RunLengthCompression.cs similarity index 82% rename from src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs rename to src/ImageSharp/Formats/OpenExr/Compression/Decompressors/RunLengthCompression.cs index 08126c4de5..0838dc1dcf 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/RunLengthCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/RunLengthCompression.cs @@ -5,12 +5,14 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; internal class RunLengthCompression : ExrBaseDecompressor { private readonly IMemoryOwner tmpBuffer; + private readonly ushort[] s = new ushort[16]; + public RunLengthCompression(MemoryAllocator allocator, uint uncompressedBytes) : base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate((int)uncompressedBytes); @@ -34,12 +36,6 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, return; } - // Check the input buffer is big enough to contain 'count' bytes of remaining data. - if (compressedBytes < 0) - { - return; - } - for (int i = 0; i < count; i++) { uncompressed[offset + i] = ReadNextByte(stream); @@ -58,12 +54,6 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, return; } - // Check the input buffer is big enough to contain byte to be duplicated. - if (compressedBytes < 0) - { - return; - } - for (int i = 0; i < count + 1; i++) { uncompressed[offset + i] = value; diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs similarity index 86% rename from src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs rename to src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs index d90f684e64..e315af8e8f 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Compressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; +namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; internal class ZipExrCompression : ExrBaseDecompressor { @@ -21,15 +21,15 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span uncompressed = this.tmpBuffer.GetSpan(); long pos = stream.Position; - using ZlibInflateStream deframeStream = new( + using ZlibInflateStream inflateStream = new( stream, () => { int left = (int)(compressedBytes - (stream.Position - pos)); return left > 0 ? left : 0; }); - deframeStream.AllocateNewBytes((int)this.UncompressedBytes, true); - DeflateStream dataStream = deframeStream.CompressedStream; + inflateStream.AllocateNewBytes((int)this.UncompressedBytes, true); + DeflateStream dataStream = inflateStream.CompressedStream; int totalRead = 0; while (totalRead < buffer.Length) diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs index 2d01efd87b..538231548c 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs @@ -1,14 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.OpenExr.Compression.Compressors; +using SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.OpenExr.Compression; internal static class ExrDecompressorFactory { - public static ExrBaseDecompressor Create(ExrCompressionType method, MemoryAllocator memoryAllocator, uint uncompressedBytes) + public static ExrBaseDecompressor Create(ExrCompressionType method, MemoryAllocator memoryAllocator, uint uncompressedBytes, int width, int height, uint rowsPerBlock, int channelCount) { switch (method) { @@ -20,6 +20,8 @@ public static ExrBaseDecompressor Create(ExrCompressionType method, MemoryAlloca return new ZipExrCompression(memoryAllocator, uncompressedBytes); case ExrCompressionType.RunLengthEncoded: return new RunLengthCompression(memoryAllocator, uncompressedBytes); + case ExrCompressionType.B44: + return new B44Compression(memoryAllocator, uncompressedBytes, width, height, rowsPerBlock, channelCount); default: throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)); } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index ca244c5674..2304f3f203 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -57,12 +57,24 @@ public ExrDecoderCore(ExrDecoderOptions options) /// public Size Dimensions => new(this.Width, this.Height); + /// + /// Gets or sets the image width. + /// private int Width { get; set; } + /// + /// Gets or sets the image height. + /// private int Height { get; set; } + /// + /// Gets or sets the image channel info's. + /// private IList Channels { get; set; } + /// + /// Gets or sets the compression method. + /// private ExrCompressionType Compression { get; set; } private ExrImageDataType ImageDataType { get; set; } @@ -76,11 +88,15 @@ public Image Decode(BufferedReadStream stream, CancellationToken where TPixel : unmanaged, IPixel { this.ReadExrHeader(stream); - this.IsSupportedCompression(); + if (!this.IsSupportedCompression()) + { + ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); + } + ExrPixelType pixelType = this.ValidateChannels(); this.ReadImageDataType(); - Image image = new Image(this.configuration, this.Width, this.Height, this.metadata); + Image image = new (this.configuration, this.Width, this.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); switch (pixelType) @@ -109,6 +125,7 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf uint bytesPerBlock = bytesPerRow * rowsPerBlock; int width = this.Width; int height = this.Height; + int channelCount = this.Channels.Count; using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); @@ -118,7 +135,7 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); - using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock, width, height, rowsPerBlock, channelCount); TPixel color = default; for (uint y = 0; y < height; y += rowsPerBlock) @@ -164,6 +181,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe uint bytesPerBlock = bytesPerRow * rowsPerBlock; int width = this.Width; int height = this.Height; + int channelCount = this.Channels.Count; using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); @@ -173,7 +191,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); - using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock, width, height, rowsPerBlock, channelCount); TPixel color = default; for (uint y = 0; y < height; y += rowsPerBlock) @@ -609,12 +627,19 @@ private ExrPixelType FindPixelType() return pixelType.Value; } - private void IsSupportedCompression() + private bool IsSupportedCompression() { - if (this.Compression is not ExrCompressionType.None and not ExrCompressionType.Zips and not ExrCompressionType.Zip and not ExrCompressionType.RunLengthEncoded) + switch (this.Compression) { - ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); + case ExrCompressionType.None: + case ExrCompressionType.Zip: + case ExrCompressionType.Zips: + case ExrCompressionType.RunLengthEncoded: + case ExrCompressionType.B44: + return true; } + + return false; } private void ReadImageDataType() diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs index 76e186eb8a..21fb0ade49 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Buffers.Binary; +using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.OpenExr.Compression; using SixLabors.ImageSharp.Memory; @@ -162,14 +163,14 @@ private void EncodeUnsignedIntPixelData(Stream stream, Buffer2D Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); - var rgb = default(Rgb96); + Rgb96 rgb = default; for (int y = 0; y < height; y++) { Span pixelRowSpan = pixels.DangerousGetRowSpan(y); for (int x = 0; x < width; x++) { - var vector4 = pixelRowSpan[x].ToVector4(); + Vector4 vector4 = pixelRowSpan[x].ToVector4(); rgb.FromVector4(vector4); redBuffer[x] = rgb.R; @@ -336,10 +337,10 @@ private void WriteDisplayWindow(Stream stream, ExrBox2i displayWindow) private void WriteAttributeInformation(Stream stream, string name, string type, int size) { // Write attribute name. - this.WriteString(stream, name); + WriteString(stream, name); // Write attribute type. - this.WriteString(stream, type); + WriteString(stream, type); // Write attribute size. BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)size); @@ -348,7 +349,7 @@ private void WriteAttributeInformation(Stream stream, string name, string type, private void WriteChannelInfo(Stream stream, ExrChannelInfo channelInfo) { - this.WriteString(stream, channelInfo.ChannelName); + WriteString(stream, channelInfo.ChannelName); BinaryPrimitives.WriteInt32LittleEndian(this.buffer, (int)channelInfo.PixelType); stream.Write(this.buffer.AsSpan(0, 4)); @@ -367,7 +368,7 @@ private void WriteChannelInfo(Stream stream, ExrChannelInfo channelInfo) stream.Write(this.buffer.AsSpan(0, 4)); } - private void WriteString(Stream stream, string str) + private static void WriteString(Stream stream, string str) { foreach (char c in str) { diff --git a/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs b/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs index 62f9f1b2f4..b609d0038c 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; - namespace SixLabors.ImageSharp.Formats.OpenExr; /// @@ -10,21 +8,15 @@ namespace SixLabors.ImageSharp.Formats.OpenExr; /// internal static class ExrThrowHelper { - [MethodImpl(InliningOptions.ColdPath)] public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}"); - [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"); - [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/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index 709a442630..29b75bc9cd 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -51,4 +51,14 @@ public void ExrDecoder_CanDecode_RunLengthCompressed(TestImageProvider(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider); + } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 8922e3ebee..25187b0fc2 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1005,5 +1005,6 @@ public static class Exr public const string Zip = "Exr/Calliphora_zip.exr"; public const string Zips = "Exr/Calliphora_zips.exr"; public const string Rle = "Exr/Calliphora_rle.exr"; + public const string B44 = "Exr/Calliphora_b44.exr"; } } diff --git a/tests/Images/Input/Exr/Calliphora_b44.exr b/tests/Images/Input/Exr/Calliphora_b44.exr new file mode 100644 index 0000000000..ebe464170f --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_b44.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cde761051241cba1e7f8466d71602abdb130677de55602fe6afec4b287c75b9d +size 2533521 From ec8163a3fabe4880347286b6fb7edd7c7c972335 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 23 Sep 2022 19:49:00 +0200 Subject: [PATCH 18/66] Fix warnings --- .../Formats/OpenExr/ExrAttribute.cs | 3 +- .../Formats/OpenExr/ExrDecoderCore.cs | 87 ++--- .../PixelImplementations/Rgb96.cs | 324 +++++++++-------- .../PixelImplementations/Rgba128.cs | 340 +++++++++--------- .../Formats/Exr/ImageExtensionsTest.cs | 3 +- 5 files changed, 377 insertions(+), 380 deletions(-) diff --git a/src/ImageSharp/Formats/OpenExr/ExrAttribute.cs b/src/ImageSharp/Formats/OpenExr/ExrAttribute.cs index ad5a8290dc..0d6ca86cc9 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrAttribute.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrAttribute.cs @@ -1,5 +1,5 @@ // Copyright (c) Six Labors. -// Licensed under the Six Labors Split License.. +// Licensed under the Six Labors Split License. using System.Diagnostics; @@ -23,4 +23,3 @@ public ExrAttribute(string name, string type, int length) public int Length { get; } } - diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index 2304f3f203..3e554fb74d 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -96,7 +96,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken ExrPixelType pixelType = this.ValidateChannels(); this.ReadImageDataType(); - Image image = new (this.configuration, this.Width, this.Height, this.metadata); + Image image = new(this.configuration, this.Width, this.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); switch (pixelType) @@ -156,16 +156,15 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { ExrChannelInfo channel = this.Channels[channelIdx]; - offset += this.ReadFloatChannelData(stream, channel, decompressedPixelData.Slice(offset), redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); + offset += ReadFloatChannelData(stream, channel, decompressedPixelData.Slice(offset), redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); } for (int x = 0; x < width; x++) { - var pixelValue = new HalfVector4(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1.0f); + HalfVector4 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1.0f); color.FromVector4(pixelValue.ToVector4()); pixelRow[x] = color; } - } stream.Position = nextRowOffsetPosition; @@ -219,7 +218,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe for (int x = 0; x < width; x++) { - var pixelValue = new Rgba128(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue); + Rgba128 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue); color.FromVector4(pixelValue.ToVector4()); pixelRow[x] = color; } @@ -227,7 +226,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe } } - private int ReadFloatChannelData( + private static int ReadFloatChannelData( BufferedReadStream stream, ExrChannelInfo channel, Span decompressedPixelData, @@ -240,19 +239,19 @@ private int ReadFloatChannelData( switch (channel.ChannelName) { case ExrConstants.ChannelNames.Red: - return this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); + return ReadChannelData(channel, decompressedPixelData, redPixelData, width); case ExrConstants.ChannelNames.Blue: - return this.ReadChannelData(channel, decompressedPixelData, bluePixelData, width); + return ReadChannelData(channel, decompressedPixelData, bluePixelData, width); case ExrConstants.ChannelNames.Green: - return this.ReadChannelData(channel, decompressedPixelData, greenPixelData, width); + return ReadChannelData(channel, decompressedPixelData, greenPixelData, width); case ExrConstants.ChannelNames.Alpha: - return this.ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); + return ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); case ExrConstants.ChannelNames.Luminance: - int bytesRead = this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); + int bytesRead = ReadChannelData(channel, decompressedPixelData, redPixelData, width); redPixelData.CopyTo(bluePixelData); redPixelData.CopyTo(greenPixelData); @@ -279,19 +278,19 @@ private int ReadUnsignedIntChannelData( switch (channel.ChannelName) { case ExrConstants.ChannelNames.Red: - return this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); + return ReadChannelData(channel, decompressedPixelData, redPixelData, width); case ExrConstants.ChannelNames.Blue: - return this.ReadChannelData(channel, decompressedPixelData, bluePixelData, width); + return ReadChannelData(channel, decompressedPixelData, bluePixelData, width); case ExrConstants.ChannelNames.Green: - return this.ReadChannelData(channel, decompressedPixelData, greenPixelData, width); + return ReadChannelData(channel, decompressedPixelData, greenPixelData, width); case ExrConstants.ChannelNames.Alpha: - return this.ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); + return ReadChannelData(channel, decompressedPixelData, alphaPixelData, width); case ExrConstants.ChannelNames.Luminance: - int bytesRead = this.ReadChannelData(channel, decompressedPixelData, redPixelData, width); + int bytesRead = ReadChannelData(channel, decompressedPixelData, redPixelData, width); redPixelData.CopyTo(bluePixelData); redPixelData.CopyTo(greenPixelData); return bytesRead; @@ -304,31 +303,31 @@ private int ReadUnsignedIntChannelData( } } - private int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) + private static int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) { switch (channel.PixelType) { case ExrPixelType.Half: - return this.ReadPixelRowChannelHalfSingle(decompressedPixelData, pixelData, width); + return ReadPixelRowChannelHalfSingle(decompressedPixelData, pixelData, width); case ExrPixelType.Float: - return this.ReadPixelRowChannelSingle(decompressedPixelData, pixelData, width); + return ReadPixelRowChannelSingle(decompressedPixelData, pixelData, width); } return 0; } - private int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) + private static int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) { switch (channel.PixelType) { case ExrPixelType.UnsignedInt: - return this.ReadPixelRowChannelUnsignedInt(decompressedPixelData, pixelData, width); + return ReadPixelRowChannelUnsignedInt(decompressedPixelData, pixelData, width); } return 0; } - private int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span channelData, int width) + private static int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span channelData, int width) { int offset = 0; for (int x = 0; x < width; x++) @@ -341,7 +340,7 @@ private int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span return offset; } - private int ReadPixelRowChannelSingle(Span decompressedPixelData, Span channelData, int width) + private static int ReadPixelRowChannelSingle(Span decompressedPixelData, Span channelData, int width) { int offset = 0; for (int x = 0; x < width; x++) @@ -354,7 +353,7 @@ private int ReadPixelRowChannelSingle(Span decompressedPixelData, Span decompressedPixelData, Span channelData, int width) + private static int ReadPixelRowChannelUnsignedInt(Span decompressedPixelData, Span channelData, int width) { int offset = 0; for (int x = 0; x < width; x++) @@ -449,7 +448,7 @@ private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) { ExrAttribute attribute = this.ReadAttribute(stream); - var header = new ExrHeaderAttributes(); + ExrHeaderAttributes header = new(); while (!attribute.Equals(ExrAttribute.EmptyAttribute)) { @@ -471,7 +470,7 @@ private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) header.DisplayWindow = displayWindow; break; case ExrConstants.AttributeNames.LineOrder: - var lineOrder = (ExrLineOrder)stream.ReadByte(); + ExrLineOrder lineOrder = (ExrLineOrder)stream.ReadByte(); header.LineOrder = lineOrder; break; case ExrConstants.AttributeNames.PixelAspectRatio: @@ -509,7 +508,7 @@ private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) private ExrAttribute ReadAttribute(BufferedReadStream stream) { string attributeName = ReadString(stream); - if (attributeName.Equals(string.Empty)) + if (attributeName.Equals(string.Empty, StringComparison.Ordinal)) { return ExrAttribute.EmptyAttribute; } @@ -533,7 +532,7 @@ private ExrBox2i ReadBoxInteger(BufferedReadStream stream) private List ReadChannelList(BufferedReadStream stream, int attributeSize) { - var channels = new List(); + List channels = new(); while (attributeSize > 1) { ExrChannelInfo channelInfo = this.ReadChannelInfo(stream, out int bytesRead); @@ -552,7 +551,7 @@ private ExrChannelInfo ReadChannelInfo(BufferedReadStream stream, out int bytesR string channelName = ReadString(stream); bytesRead = channelName.Length + 1; - var pixelType = (ExrPixelType)this.ReadSignedInteger(stream); + ExrPixelType pixelType = (ExrPixelType)this.ReadSignedInteger(stream); bytesRead += 4; byte pLinear = (byte)stream.ReadByte(); @@ -572,7 +571,7 @@ private ExrChannelInfo ReadChannelInfo(BufferedReadStream stream, out int bytesR private static string ReadString(BufferedReadStream stream) { - var str = new StringBuilder(); + StringBuilder str = new(); int character = stream.ReadByte(); if (character == 0) { @@ -599,11 +598,11 @@ private ExrPixelType FindPixelType() 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) || - this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Luminance)) + if (this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Blue, StringComparison.Ordinal) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Green, StringComparison.Ordinal) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Red, StringComparison.Ordinal) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Alpha, StringComparison.Ordinal) || + this.Channels[i].ChannelName.Equals(ExrConstants.ChannelNames.Luminance, StringComparison.Ordinal)) { if (!pixelType.HasValue) { @@ -651,27 +650,27 @@ private void ReadImageDataType() bool hasLuminance = false; foreach (ExrChannelInfo channelInfo in this.Channels) { - if (channelInfo.ChannelName.Equals("A")) + if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal)) { hasAlphaChannel = true; } - if (channelInfo.ChannelName.Equals("R")) + if (channelInfo.ChannelName.Equals("R", StringComparison.Ordinal)) { hasRedChannel = true; } - if (channelInfo.ChannelName.Equals("G")) + if (channelInfo.ChannelName.Equals("G", StringComparison.Ordinal)) { hasGreenChannel = true; } - if (channelInfo.ChannelName.Equals("B")) + if (channelInfo.ChannelName.Equals("B", StringComparison.Ordinal)) { hasBlueChannel = true; } - if (channelInfo.ChannelName.Equals("Y")) + if (channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal)) { hasLuminance = true; } @@ -702,7 +701,7 @@ private bool HasAlpha() { foreach (ExrChannelInfo channelInfo in this.Channels) { - if (channelInfo.ChannelName.Equals("A")) + if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal)) { return true; } @@ -716,7 +715,11 @@ private uint CalculateBytesPerRow(uint width) uint bytesPerRow = 0; foreach (ExrChannelInfo channelInfo in this.Channels) { - if (channelInfo.ChannelName.Equals("A") || channelInfo.ChannelName.Equals("R") || channelInfo.ChannelName.Equals("G") || channelInfo.ChannelName.Equals("B") || channelInfo.ChannelName.Equals("Y")) + if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("R", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("G", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("B", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal)) { if (channelInfo.PixelType == ExrPixelType.Half) { diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs index 2809a4c319..620e305a21 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs @@ -1,175 +1,173 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.PixelFormats +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 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; + + /// + /// 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); + /// - /// 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. - /// + /// Compares two objects for equality. /// - [StructLayout(LayoutKind.Sequential)] - public partial struct Rgb96 : IPixel + /// 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) { - 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; - - /// - /// 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) - { - 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); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ToVector4() => new( - this.R * InvMax, - this.G * InvMax, - this.B * InvMax, - 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})"); + 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); } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ToVector4() => new( + this.R * InvMax, + this.G * InvMax, + this.B * InvMax, + 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})"); } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs index 01fc16d45a..912a62db0c 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs @@ -1,183 +1,181 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. -using System; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.PixelFormats +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; + /// - /// 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. - /// + /// Gets the alpha channel. /// - [StructLayout(LayoutKind.Sequential)] - public partial struct Rgba128 : IPixel + 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) { - 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})"); + 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})"); } diff --git a/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs index 323000bc59..926f5ca909 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs @@ -1,5 +1,5 @@ // Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. +// Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.OpenExr; @@ -64,4 +64,3 @@ public async Task SaveAsExrAsync_Stream_Encoder() } } } - From 28dcdf2f0c2912891d8e5b2346ed2c93a5f1d5d2 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 7 Apr 2023 21:10:05 +0200 Subject: [PATCH 19/66] Add DecodeTiledFloatingPointPixelData --- .../Formats/OpenExr/ExrDecoderCore.cs | 126 +++++++++++++++++- 1 file changed, 121 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index d414babf28..2baa60b49b 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -81,7 +81,11 @@ public Image Decode(BufferedReadStream stream, CancellationToken where TPixel : unmanaged, IPixel { this.ReadExrHeader(stream); - this.IsSupportedCompression(); + if (!this.IsSupportedCompression()) + { + ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); + } + ExrPixelType pixelType = this.ValidateChannels(); this.ReadImageDataType(); @@ -92,7 +96,15 @@ public Image Decode(BufferedReadStream stream, CancellationToken { case ExrPixelType.Half: case ExrPixelType.Float: - this.DecodeFloatingPointPixelData(stream, pixels); + if (this.ImageType is ExrImageType.ScanLine) + { + this.DecodeFloatingPointPixelData(stream, pixels); + } + else + { + this.DecodeTiledFloatingPointPixelData(stream, pixels); + } + break; case ExrPixelType.UnsignedInt: this.DecodeUnsignedIntPixelData(stream, pixels); @@ -214,6 +226,97 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe } } + private void DecodeTiledFloatingPointPixelData(BufferedReadStream stream, Buffer2D pixels) + where TPixel : unmanaged, IPixel + { + if (!this.HeaderAttributes.ChunkCount.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Missing chunk count in tiled image"); + } + + int chunks = this.HeaderAttributes.ChunkCount.Value; + + bool hasAlpha = this.HasAlpha(); + uint tileWidth = (uint)this.HeaderAttributes.TileXSize; + uint tileHeight = (uint)this.HeaderAttributes.TileYSize; + int width = this.Width; + int height = this.Height; + uint bytesPerRow = this.CalculateBytesPerRow((uint)this.Width); + uint bytesPerBlockRow = this.CalculateBytesPerRow(tileWidth); + uint rowsPerBlock = this.RowsPerBlock(); + uint bytesPerBlock = bytesPerBlockRow * rowsPerBlock; + uint columnsPerBlock = (uint)(this.Height / tileHeight); + uint tilesPerScanline = (uint)((this.Width + tileWidth - 1) / tileWidth); + + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate((int)(tileWidth * 4)); + using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((width * height) / chunks); + Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); + Span redPixelData = rowBuffer.GetSpan().Slice(0, (int)tileWidth); + Span greenPixelData = rowBuffer.GetSpan().Slice((int)tileWidth, (int)tileWidth); + Span bluePixelData = rowBuffer.GetSpan().Slice((int)(tileWidth * 2), (int)tileWidth); + Span alphaPixelData = rowBuffer.GetSpan().Slice((int)(tileWidth * 3), (int)tileWidth); + + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock); + + uint y = 0; + uint x = 0; + TPixel color = default; + for (int chunk = 0; chunk < chunks; chunk++) + { + ulong dataOffset = this.ReadUnsignedLong(stream); + long nextOffsetPosition = stream.Position; + stream.Position = (long)dataOffset; + uint tileX = this.ReadUnsignedInteger(stream); + uint tileY = this.ReadUnsignedInteger(stream); + uint levelX = this.ReadUnsignedInteger(stream); + uint levelY = this.ReadUnsignedInteger(stream); + + uint compressedBytesCount = this.ReadUnsignedInteger(stream); + decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); + + int offset = 0; + for (y = 0; y < height; y += rowsPerBlock) + { + for (x = 0; x < tilesPerScanline; x++) + { + uint rowStartIndex = tileHeight * tileY; + uint columnStartIndex = tileWidth * tileX; + for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) + { + Span pixelRow = pixels.DangerousGetRowSpan((int)rowIndex); + for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) + { + ExrChannelInfo channel = this.Channels[channelIdx]; + offset += this.ReadFloatChannelData( + stream, + channel, + decompressedPixelData.Slice(offset), + redPixelData, + greenPixelData, + bluePixelData, + alphaPixelData, + (int)tileWidth); + } + + uint columnEndIdx = (uint)Math.Min(columnStartIndex + tileWidth, width); + int channelOffset = 0; + for (int pixelRowIdx = (int)columnStartIndex; pixelRowIdx < columnEndIdx; pixelRowIdx++) + { + var pixelValue = new HalfVector4(redPixelData[channelOffset], + greenPixelData[channelOffset], bluePixelData[channelOffset], + hasAlpha ? alphaPixelData[channelOffset] : 1.0f); + color.FromVector4(pixelValue.ToVector4()); + pixelRow[pixelRowIdx] = color; + channelOffset++; + } + } + } + } + + stream.Position = nextOffsetPosition; + } + } + private int ReadFloatChannelData( BufferedReadStream stream, ExrChannelInfo channel, @@ -477,6 +580,12 @@ private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) case ExrConstants.AttributeNames.Tiles: header.TileXSize = this.ReadUnsignedInteger(stream); header.TileYSize = this.ReadUnsignedInteger(stream); + int mode = stream.ReadByte(); + if (mode != 0) + { + ExrThrowHelper.ThrowNotSupported("Unsupported tile mode. Only mode 0 is supported yet."); + } + break; case ExrConstants.AttributeNames.ChunkCount: header.ChunkCount = this.ReadSignedInteger(stream); @@ -614,12 +723,19 @@ private ExrPixelType FindPixelType() return pixelType.Value; } - private void IsSupportedCompression() + private bool IsSupportedCompression() { - if (this.Compression is not ExrCompressionType.None and not ExrCompressionType.Zips and not ExrCompressionType.Zip and not ExrCompressionType.RunLengthEncoded) + switch (this.Compression) { - ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); + case ExrCompressionType.None: + case ExrCompressionType.Zip: + case ExrCompressionType.Zips: + case ExrCompressionType.RunLengthEncoded: + case ExrCompressionType.B44: + return true; } + + return false; } private void ReadImageDataType() From fa52ab740fe97a6de382ee20792d2565ec36a512 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 14 Mar 2026 18:05:55 +0100 Subject: [PATCH 20/66] Adjust exr decoder / encoder to ImageSharp changes --- ImageSharp.sln | 4 +- src/ImageSharp/Configuration.cs | 2 +- .../Decompressors/ZipExrCompression.cs | 2 +- .../Formats/OpenExr/ExrConfigurationModule.cs | 4 +- src/ImageSharp/Formats/OpenExr/ExrDecoder.cs | 36 +++--- .../Formats/OpenExr/ExrDecoderCore.cs | 86 +++++++------- .../Formats/OpenExr/ExrDecoderOptions.cs | 2 +- src/ImageSharp/Formats/OpenExr/ExrEncoder.cs | 22 +--- .../Formats/OpenExr/ExrEncoderCore.cs | 75 +++++++----- .../Formats/OpenExr/ExrHeaderAttributes.cs | 85 ++++++-------- .../Formats/OpenExr/ExrImageFormatDetector.cs | 12 +- src/ImageSharp/Formats/OpenExr/ExrMetadata.cs | 16 ++- .../Formats/OpenExr/MetadataExtensions.cs | 20 ---- .../_Generated/ImageExtensions.Save.cs | 100 +++++++++++++--- .../_Generated/ImageMetadataExtensions.cs | 21 ++++ .../Formats/_Generated/_Formats.ttinclude | 3 +- src/ImageSharp/ImageSharp.csproj | 10 +- .../PixelImplementations/Rgb96.cs | 88 ++++---------- .../PixelImplementations/Rgba128.cs | 95 +++++---------- .../Formats/Exr/ExrDecoderTests.cs | 12 +- .../Formats/Exr/ImageExtensionsTest.cs | 108 +++++++++++++++--- .../TestUtilities/TestEnvironment.Formats.cs | 2 +- 22 files changed, 440 insertions(+), 365 deletions(-) delete mode 100644 src/ImageSharp/Formats/OpenExr/MetadataExtensions.cs diff --git a/ImageSharp.sln b/ImageSharp.sln index 7ccd92c07d..13dd2fba7e 100644 --- a/ImageSharp.sln +++ b/ImageSharp.sln @@ -37,8 +37,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{815C0625-CD3 ProjectSection(SolutionItems) = preProject src\Directory.Build.props = src\Directory.Build.props src\Directory.Build.targets = src\Directory.Build.targets - src\README.md = src\README.md src\ImageSharp.ruleset = src\ImageSharp.ruleset + src\README.md = src\README.md EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharp", "src\ImageSharp\ImageSharp.csproj", "{2AA31A1F-142C-43F4-8687-09ABCA4B3A26}" @@ -215,6 +215,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{5C9B68 ProjectSection(SolutionItems) = preProject tests\Images\Input\Jpg\issues\issue-1076-invalid-subsampling.jpg = tests\Images\Input\Jpg\issues\issue-1076-invalid-subsampling.jpg tests\Images\Input\Jpg\issues\issue-1221-identify-multi-frame.jpg = tests\Images\Input\Jpg\issues\issue-1221-identify-multi-frame.jpg + tests\Images\Input\Jpg\issues\issue-2067-comment.jpg = tests\Images\Input\Jpg\issues\issue-2067-comment.jpg tests\Images\Input\Jpg\issues\issue1006-incorrect-resize.jpg = tests\Images\Input\Jpg\issues\issue1006-incorrect-resize.jpg tests\Images\Input\Jpg\issues\issue1049-exif-resize.jpg = tests\Images\Input\Jpg\issues\issue1049-exif-resize.jpg tests\Images\Input\Jpg\issues\Issue159-MissingFF00-Progressive-Bedroom.jpg = tests\Images\Input\Jpg\issues\Issue159-MissingFF00-Progressive-Bedroom.jpg @@ -238,7 +239,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "issues", "issues", "{5C9B68 tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg = tests\Images\Input\Jpg\issues\issue750-exif-tranform.jpg tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg = tests\Images\Input\Jpg\issues\Issue845-Incorrect-Quality99.jpg tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg = tests\Images\Input\Jpg\issues\issue855-incorrect-colorspace.jpg - tests\Images\Input\Jpg\issues\issue-2067-comment.jpg = tests\Images\Input\Jpg\issues\issue-2067-comment.jpg EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "fuzz", "fuzz", "{516A3532-6AC2-417B-AD79-9BD5D0D378A0}" diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index abcbcb58f8..7a2374fb6a 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -226,7 +226,7 @@ public void Configure(IImageFormatConfigurationModule configuration) new TgaConfigurationModule(), new TiffConfigurationModule(), new WebpConfigurationModule(), - new ExrConfigurationModule()); + new ExrConfigurationModule(), new QoiConfigurationModule(), new IcoConfigurationModule(), new CurConfigurationModule()); diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs index e315af8e8f..1797bd44b3 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs @@ -29,7 +29,7 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, return left > 0 ? left : 0; }); inflateStream.AllocateNewBytes((int)this.UncompressedBytes, true); - DeflateStream dataStream = inflateStream.CompressedStream; + DeflateStream dataStream = inflateStream.CompressedStream!; int totalRead = 0; while (totalRead < buffer.Length) diff --git a/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs b/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs index 0b0058805d..c0322728aa 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs @@ -6,13 +6,13 @@ namespace SixLabors.ImageSharp.Formats.OpenExr; /// /// Registers the image encoders, decoders and mime type detectors for the OpenExr format. /// -public sealed class ExrConfigurationModule : IConfigurationModule +public sealed class ExrConfigurationModule : IImageFormatConfigurationModule { /// public void Configure(Configuration configuration) { configuration.ImageFormatsManager.SetEncoder(ExrFormat.Instance, new ExrEncoder()); - configuration.ImageFormatsManager.SetDecoder(ExrFormat.Instance, new ExrDecoder()); + configuration.ImageFormatsManager.SetDecoder(ExrFormat.Instance, ExrDecoder.Instance); configuration.ImageFormatsManager.AddImageFormatDetector(new ExrImageFormatDetector()); } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoder.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoder.cs index dcbe80c277..84d441eb72 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoder.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoder.cs @@ -8,39 +8,41 @@ namespace SixLabors.ImageSharp.Formats.OpenExr; /// /// Image decoder for generating an image out of a OpenExr stream. /// -public class ExrDecoder : IImageDecoderSpecialized +public class ExrDecoder : ImageDecoder { + private ExrDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static ExrDecoder Instance { get; } = new(); + /// - IImageInfo IImageInfoDetector.Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); - return new ExrDecoderCore(new() { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken); + return new ExrDecoderCore(new ExrDecoderOptions { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken); } - /// - Image IImageDecoder.Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - => ((IImageDecoderSpecialized)this).Decode(new() { GeneralOptions = options }, stream, cancellationToken); - - /// - Image IImageDecoder.Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) - => ((IImageDecoderSpecialized)this).Decode(new() { GeneralOptions = options }, stream, cancellationToken); - - /// - Image IImageDecoderSpecialized.Decode(ExrDecoderOptions options, Stream stream, CancellationToken cancellationToken) + //// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); - Image image = new ExrDecoderCore(options).Decode(options.GeneralOptions.Configuration, stream, cancellationToken); + ExrDecoderCore decoder = new(new ExrDecoderOptions { GeneralOptions = options }); + Image image = decoder.Decode(options.Configuration, stream, cancellationToken); - ImageDecoderUtilities.Resize(options.GeneralOptions, image); + ScaleToTargetSize(options, image); return image; } /// - Image IImageDecoderSpecialized.Decode(ExrDecoderOptions options, Stream stream, CancellationToken cancellationToken) - => ((IImageDecoderSpecialized)this).Decode(options, stream, cancellationToken); + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs index 3e554fb74d..6eaf56fde3 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs @@ -1,5 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#nullable disable using System.Buffers; using System.Buffers.Binary; @@ -16,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr; /// /// Performs the OpenExr decoding operation. /// -internal sealed class ExrDecoderCore : IImageDecoderInternals +internal sealed class ExrDecoderCore : ImageDecoderCore { /// /// Reusable buffer. @@ -43,15 +44,12 @@ internal sealed class ExrDecoderCore : IImageDecoderInternals /// /// The options. public ExrDecoderCore(ExrDecoderOptions options) + : base(options.GeneralOptions) { - this.Options = options.GeneralOptions; this.configuration = options.GeneralOptions.Configuration; this.memoryAllocator = this.configuration.MemoryAllocator; } - /// - public DecoderOptions Options { get; } - /// /// Gets the dimensions of the image. /// @@ -84,8 +82,7 @@ public ExrDecoderCore(ExrDecoderOptions options) private ExrHeaderAttributes HeaderAttributes { get; set; } /// - public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) { this.ReadExrHeader(stream); if (!this.IsSupportedCompression()) @@ -162,7 +159,7 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf for (int x = 0; x < width; x++) { HalfVector4 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1.0f); - color.FromVector4(pixelValue.ToVector4()); + TPixel.FromVector4(pixelValue.ToVector4()); pixelRow[x] = color; } } @@ -219,7 +216,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe for (int x = 0; x < width; x++) { Rgba128 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue); - color.FromVector4(pixelValue.ToVector4()); + TPixel.FromVector4(pixelValue.ToVector4()); pixelRow[x] = color; } } @@ -366,13 +363,13 @@ private static int ReadPixelRowChannelUnsignedInt(Span decompressedPixelDa } /// - public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) { ExrHeaderAttributes header = this.ReadExrHeader(stream); int bitsPerPixel = this.CalculateBitsPerPixel(); - return new ImageInfo(new PixelTypeInfo(bitsPerPixel), this.Width, this.Height, new ImageMetadata()); + return new ImageInfo(new Size((int)header.ScreenWindowWidth, (int)header.AspectRatio), this.metadata); } private int CalculateBitsPerPixel() @@ -430,15 +427,10 @@ private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) this.HeaderAttributes = this.ParseHeaderAttributes(stream); - if (!this.HeaderAttributes.IsValid()) - { - ExrThrowHelper.ThrowInvalidImageHeader(); - } - - this.Width = this.HeaderAttributes.DataWindow.Value.XMax - this.HeaderAttributes.DataWindow.Value.XMin + 1; - this.Height = this.HeaderAttributes.DataWindow.Value.YMax - this.HeaderAttributes.DataWindow.Value.YMin + 1; + this.Width = this.HeaderAttributes.DataWindow.XMax - this.HeaderAttributes.DataWindow.XMin + 1; + this.Height = this.HeaderAttributes.DataWindow.YMax - this.HeaderAttributes.DataWindow.YMin + 1; this.Channels = this.HeaderAttributes.Channels; - this.Compression = this.HeaderAttributes.Compression.GetValueOrDefault(); + this.Compression = this.HeaderAttributes.Compression; this.metadata = new ImageMetadata(); @@ -448,50 +440,54 @@ private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) { ExrAttribute attribute = this.ReadAttribute(stream); - ExrHeaderAttributes header = new(); + IList channels = null; + ExrBox2i? dataWindow = null; + ExrCompressionType? compression = null; + ExrBox2i? displayWindow = null; + ExrLineOrder? lineOrder = null; + float? aspectRatio = null; + float? screenWindowCenterX = null; + float? screenWindowCenterY = null; + float? screenWindowWidth = null; + uint? tileXSize = null; + uint? tileYSize = null; + int? chunkCount = null; while (!attribute.Equals(ExrAttribute.EmptyAttribute)) { switch (attribute.Name) { case ExrConstants.AttributeNames.Channels: - IList channels = this.ReadChannelList(stream, attribute.Length); - header.Channels = channels; + channels = this.ReadChannelList(stream, attribute.Length); break; case ExrConstants.AttributeNames.Compression: - header.Compression = (ExrCompressionType)stream.ReadByte(); + compression = (ExrCompressionType)stream.ReadByte(); break; case ExrConstants.AttributeNames.DataWindow: - ExrBox2i dataWindow = this.ReadBoxInteger(stream); - header.DataWindow = dataWindow; + dataWindow = this.ReadBoxInteger(stream); break; case ExrConstants.AttributeNames.DisplayWindow: - ExrBox2i displayWindow = this.ReadBoxInteger(stream); - header.DisplayWindow = displayWindow; + displayWindow = this.ReadBoxInteger(stream); break; case ExrConstants.AttributeNames.LineOrder: - ExrLineOrder lineOrder = (ExrLineOrder)stream.ReadByte(); - header.LineOrder = lineOrder; + lineOrder = (ExrLineOrder)stream.ReadByte(); break; case ExrConstants.AttributeNames.PixelAspectRatio: - float aspectRatio = stream.ReadSingle(this.buffer); - header.AspectRatio = aspectRatio; + aspectRatio = stream.ReadSingle(this.buffer); break; case ExrConstants.AttributeNames.ScreenWindowCenter: - float screenWindowCenterX = stream.ReadSingle(this.buffer); - float screenWindowCenterY = stream.ReadSingle(this.buffer); - header.ScreenWindowCenter = new PointF(screenWindowCenterX, screenWindowCenterY); + screenWindowCenterX = stream.ReadSingle(this.buffer); + screenWindowCenterY = stream.ReadSingle(this.buffer); break; case ExrConstants.AttributeNames.ScreenWindowWidth: - float screenWindowWidth = stream.ReadSingle(this.buffer); - header.ScreenWindowWidth = screenWindowWidth; + screenWindowWidth = stream.ReadSingle(this.buffer); break; case ExrConstants.AttributeNames.Tiles: - header.TileXSize = this.ReadUnsignedInteger(stream); - header.TileYSize = this.ReadUnsignedInteger(stream); + tileXSize = this.ReadUnsignedInteger(stream); + tileYSize = this.ReadUnsignedInteger(stream); break; case ExrConstants.AttributeNames.ChunkCount: - header.ChunkCount = this.ReadSignedInteger(stream); + chunkCount = this.ReadSignedInteger(stream); break; default: // Skip unknown attribute bytes. @@ -502,6 +498,18 @@ private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) attribute = this.ReadAttribute(stream); } + ExrHeaderAttributes header = new( + channels, + compression.Value, + dataWindow.Value, + displayWindow.Value, + lineOrder.Value, + aspectRatio.Value, + screenWindowWidth.Value, + new PointF(screenWindowCenterX.Value, screenWindowCenterY.Value), + tileXSize, + tileYSize, + chunkCount); return header; } diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderOptions.cs b/src/ImageSharp/Formats/OpenExr/ExrDecoderOptions.cs index f29e9cfb92..c0b419b937 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderOptions.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrDecoderOptions.cs @@ -9,5 +9,5 @@ namespace SixLabors.ImageSharp.Formats.OpenExr; public sealed class ExrDecoderOptions : ISpecializedDecoderOptions { /// - public DecoderOptions GeneralOptions { get; set; } = new(); + public DecoderOptions GeneralOptions { get; init; } = new(); } diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoder.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoder.cs index 54145afdac..9960efd686 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoder.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoder.cs @@ -1,34 +1,22 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; -using SixLabors.ImageSharp.PixelFormats; - namespace SixLabors.ImageSharp.Formats.OpenExr; /// /// Image encoder for writing an image to a stream in the OpenExr Format. /// -public sealed class ExrEncoder : IImageEncoder, IExrEncoderOptions +public sealed class ExrEncoder : ImageEncoder { /// /// Gets or sets the pixel type of the image. /// public ExrPixelType? PixelType { get; set; } - /// - public void Encode(Image image, Stream stream) - where TPixel : unmanaged, IPixel - { - ExrEncoderCore encoder = new(this, image.GetMemoryAllocator()); - encoder.Encode(image, stream); - } - - /// - public Task EncodeAsync(Image image, Stream stream, CancellationToken cancellationToken) - where TPixel : unmanaged, IPixel + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - ExrEncoderCore encoder = new(this, image.GetMemoryAllocator()); - return encoder.EncodeAsync(image, stream, cancellationToken); + ExrEncoderCore encoder = new(this, image.Configuration, image.Configuration.MemoryAllocator); + encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs index 21fb0ade49..740b71e12b 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs @@ -5,6 +5,7 @@ using System.Buffers.Binary; using System.Numerics; using System.Runtime.CompilerServices; +using System.Threading.Channels; using SixLabors.ImageSharp.Formats.OpenExr.Compression; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -15,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats.OpenExr; /// /// Image encoder for writing an image to a stream in the OpenExr format. /// -internal sealed class ExrEncoderCore : IImageEncoderInternals +internal sealed class ExrEncoderCore { /// /// Reusable buffer. @@ -27,6 +28,16 @@ internal sealed class ExrEncoderCore : IImageEncoderInternals /// private readonly MemoryAllocator memoryAllocator; + /// + /// The global configuration. + /// + private Configuration configuration; + + /// + /// The encoder with options. + /// + private readonly ExrEncoder encoder; + /// /// The pixel type of the image. /// @@ -35,12 +46,14 @@ internal sealed class ExrEncoderCore : IImageEncoderInternals /// /// Initializes a new instance of the class. /// - /// The encoder options. + /// The encoder with options. + /// The configuration. /// The memory manager. - public ExrEncoderCore(IExrEncoderOptions options, MemoryAllocator memoryAllocator) + public ExrEncoderCore(ExrEncoder encoder, Configuration configuration, MemoryAllocator memoryAllocator) { + this.configuration = configuration; + this.encoder = encoder; this.memoryAllocator = memoryAllocator; - this.pixelType = options.PixelType; } /// @@ -63,22 +76,28 @@ public void Encode(Image image, Stream stream, CancellationToken this.pixelType ??= exrMetadata.PixelType; int width = image.Width; int height = image.Height; - var header = new ExrHeaderAttributes() - { - Compression = ExrCompressionType.None, - AspectRatio = 1.0f, - DataWindow = new ExrBox2i(0, 0, width - 1, height - 1), - DisplayWindow = new ExrBox2i(0, 0, width - 1, height - 1), - LineOrder = ExrLineOrder.IncreasingY, - ScreenWindowCenter = new PointF(0.0f, 0.0f), - ScreenWindowWidth = 1, - Channels = new List() - { - 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), - } - }; + ExrCompressionType compression = ExrCompressionType.None; + float aspectRatio = 1.0f; + ExrBox2i dataWindow = new(0, 0, width - 1, height - 1); + ExrBox2i displayWindow = new(0, 0, width - 1, height - 1); + ExrLineOrder lineOrder = ExrLineOrder.IncreasingY; + PointF screenWindowCenter = new(0.0f, 0.0f); + int screenWindowWidth = 1; + List channels = + [ + 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), + ]; + ExrHeaderAttributes header = new( + channels, + compression, + dataWindow, + displayWindow, + lineOrder, + aspectRatio, + screenWindowWidth, + screenWindowCenter); // Write magick bytes. BinaryPrimitives.WriteInt32LittleEndian(this.buffer, ExrConstants.MagickBytes); @@ -171,7 +190,7 @@ private void EncodeUnsignedIntPixelData(Stream stream, Buffer2D for (int x = 0; x < width; x++) { Vector4 vector4 = pixelRowSpan[x].ToVector4(); - rgb.FromVector4(vector4); + Rgb96.FromVector4(vector4); redBuffer[x] = rgb.R; greenBuffer[x] = rgb.G; @@ -193,13 +212,13 @@ private void EncodeUnsignedIntPixelData(Stream stream, Buffer2D private void WriteHeader(Stream stream, ExrHeaderAttributes header) { this.WriteChannels(stream, header.Channels); - this.WriteCompression(stream, header.Compression.Value); - this.WriteDataWindow(stream, header.DataWindow.Value); - this.WriteDisplayWindow(stream, header.DisplayWindow.Value); - this.WritePixelAspectRatio(stream, header.AspectRatio.Value); - this.WriteLineOrder(stream, header.LineOrder.Value); - this.WriteScreenWindowCenter(stream, header.ScreenWindowCenter.Value); - this.WriteScreenWindowWidth(stream, header.ScreenWindowWidth.Value); + this.WriteCompression(stream, header.Compression); + this.WriteDataWindow(stream, header.DataWindow); + this.WriteDisplayWindow(stream, header.DisplayWindow); + this.WritePixelAspectRatio(stream, header.AspectRatio); + this.WriteLineOrder(stream, header.LineOrder); + this.WriteScreenWindowCenter(stream, header.ScreenWindowCenter); + this.WriteScreenWindowWidth(stream, header.ScreenWindowWidth); stream.WriteByte(0); } diff --git a/src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs b/src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs index b3b3cdb604..4cec4fbc58 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs @@ -7,70 +7,51 @@ namespace SixLabors.ImageSharp.Formats.OpenExr; internal class ExrHeaderAttributes { + public ExrHeaderAttributes( + IList channels, + ExrCompressionType compression, + ExrBox2i dataWindow, + ExrBox2i displayWindow, + ExrLineOrder lineOrder, + float aspectRatio, + float screenWindowWidth, + PointF screenWindowCenter, + uint? tileXSize = null, + uint? tileYSize = null, + int? chunkCount = null) + { + this.Channels = channels; + this.Compression = compression; + this.DataWindow = dataWindow; + this.DisplayWindow = displayWindow; + this.LineOrder = lineOrder; + this.AspectRatio = aspectRatio; + this.ScreenWindowWidth = screenWindowWidth; + this.ScreenWindowCenter = screenWindowCenter; + this.TileXSize = tileXSize; + this.TileYSize = tileYSize; + this.ChunkCount = chunkCount; + } + public IList Channels { get; set; } - public ExrCompressionType? Compression { get; set; } + public ExrCompressionType Compression { get; set; } - public ExrBox2i? DataWindow { get; set; } + public ExrBox2i DataWindow { get; set; } - public ExrBox2i? DisplayWindow { get; set; } + public ExrBox2i DisplayWindow { get; set; } - public ExrLineOrder? LineOrder { get; set; } + public ExrLineOrder LineOrder { get; set; } - public float? AspectRatio { get; set; } + public float AspectRatio { get; set; } - public float? ScreenWindowWidth { get; set; } + public float ScreenWindowWidth { get; set; } - public PointF? ScreenWindowCenter { get; set; } + public PointF ScreenWindowCenter { get; set; } public uint? TileXSize { get; set; } public uint? TileYSize { get; set; } public int? ChunkCount { 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 index c14ab947a9..2f97e74048 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrImageFormatDetector.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrImageFormatDetector.cs @@ -2,6 +2,8 @@ // Licensed under the Six Labors Split License. using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Formats.Qoi; namespace SixLabors.ImageSharp.Formats.OpenExr; @@ -13,9 +15,6 @@ 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) @@ -26,4 +25,11 @@ private bool IsSupportedFileFormat(ReadOnlySpan header) return false; } + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = this.IsSupportedFileFormat(header) ? QoiFormat.Instance : null; + return format != null; + } } diff --git a/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs b/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs index 8b090d6842..9cb58e9e67 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs +++ b/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs @@ -1,12 +1,15 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.OpenExr; /// /// Provides OpenExr specific metadata information for the image. /// -public class ExrMetadata : IDeepCloneable +public class ExrMetadata : IFormatMetadata { /// /// Initializes a new instance of the class. @@ -26,6 +29,17 @@ public ExrMetadata() /// public ExrPixelType PixelType { get; set; } = ExrPixelType.Float; + public static ExrMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException(); + + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel => throw new NotImplementedException(); + /// public IDeepCloneable DeepClone() => new ExrMetadata(this); + + public PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); + + public FormatConnectingMetadata ToFormatConnectingMetadata() => throw new NotImplementedException(); + + ExrMetadata IDeepCloneable.DeepClone() => throw new NotImplementedException(); } diff --git a/src/ImageSharp/Formats/OpenExr/MetadataExtensions.cs b/src/ImageSharp/Formats/OpenExr/MetadataExtensions.cs deleted file mode 100644 index 2af0a57b07..0000000000 --- a/src/ImageSharp/Formats/OpenExr/MetadataExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.OpenExr; -using SixLabors.ImageSharp.Metadata; - -namespace SixLabors.ImageSharp; - -/// -/// Extension methods for the type. -/// -public static partial class MetadataExtensions -{ - /// - /// Gets the open exr format specific metadata for the image. - /// - /// The metadata this method extends. - /// The . - public static ExrMetadata GetExrMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(ExrFormat.Instance); -} diff --git a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs index f3aaf6182b..ca4ad0e06c 100644 --- a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs @@ -7,13 +7,13 @@ using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.OpenExr; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Qoi; using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Formats.OpenExr; namespace SixLabors.ImageSharp; @@ -1144,24 +1144,95 @@ public static Task SaveAsWebpAsync(this Image source, Stream stream, WebpEncoder encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance), cancellationToken); - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(TiffFormat.Instance), - cancellationToken); + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// Thrown if the path is null. + public static void SaveAsExr(this Image source, string path) => SaveAsExr(source, path, default); /// - /// Saves the image to the given stream with the Open Exr format. + /// Saves the image to the given stream with the Exr format. /// /// The image this method extends. - /// The stream to save the image to. + /// The file path to save the image to. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, string path) => SaveAsExrAsync(source, path, default); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, string path, CancellationToken cancellationToken) + => SaveAsExrAsync(source, path, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. /// The encoder to save the image with. + /// Thrown if the path is null. + public static void SaveAsExr(this Image source, string path, ExrEncoder encoder) => + source.Save( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance)); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The file path to save the image to. + /// The encoder to save the image with. + /// The token to monitor for cancellation requests. + /// Thrown if the path is null. + /// A representing the asynchronous operation. + public static Task SaveAsExrAsync(this Image source, string path, ExrEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + path, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance), + cancellationToken); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// Thrown if the stream is null. + public static void SaveAsExr(this Image source, Stream stream) + => SaveAsExr(source, stream, default); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The token to monitor for cancellation requests. /// Thrown if the stream is null. /// A representing the asynchronous operation. - public static void SaveAsOpenExr(this Image source, Stream stream, ExrEncoder encoder) + public static Task SaveAsExrAsync(this Image source, Stream stream, CancellationToken cancellationToken = default) + => SaveAsExrAsync(source, stream, default, cancellationToken); + + /// + /// Saves the image to the given stream with the Exr format. + /// + /// The image this method extends. + /// The stream to save the image to. + /// The encoder to save the image with. + /// Thrown if the stream is null. + public static void SaveAsExr(this Image source, Stream stream, ExrEncoder encoder) => source.Save( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ExrFormat.Instance)); + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance)); /// - /// Saves the image to the given stream with the Open Exr format. + /// Saves the image to the given stream with the Exr format. /// /// The image this method extends. /// The stream to save the image to. @@ -1169,11 +1240,10 @@ public static void SaveAsOpenExr(this Image source, Stream stream, ExrEncoder en /// The token to monitor for cancellation requests. /// Thrown if the stream is null. /// A representing the asynchronous operation. - public static Task SaveAsOpenExrAsync(this Image source, Stream stream, ExrEncoder encoder, CancellationToken cancellationToken = default) => - source.SaveAsync( - stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.FindEncoder(ExrFormat.Instance), - cancellationToken); - + public static Task SaveAsExrAsync(this Image source, Stream stream, ExrEncoder encoder, CancellationToken cancellationToken = default) + => source.SaveAsync( + stream, + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(ExrFormat.Instance), + cancellationToken); } diff --git a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs index e35d00ed39..31a25fcede 100644 --- a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs +++ b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs @@ -14,6 +14,7 @@ using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Formats.OpenExr; namespace SixLabors.ImageSharp; @@ -242,6 +243,26 @@ public static class ImageMetadataExtensions /// The new public static WebpMetadata CloneWebpMetadata(this ImageMetadata source) => source.CloneFormatMetadata(WebpFormat.Instance); + /// + /// Gets the from .
+ /// If none is found, an instance is created either by conversion from the decoded image format metadata + /// or the requested format default constructor. + /// This instance will be added to the metadata for future requests. + ///
+ /// The image metadata. + /// + /// The + /// + public static ExrMetadata GetExrMetadata(this ImageMetadata source) => source.GetFormatMetadata(ExrFormat.Instance); + + /// + /// Creates a new cloned instance of from the . + /// The instance is created via + /// + /// The image metadata. + /// The new + public static ExrMetadata CloneExrMetadata(this ImageMetadata source) => source.CloneFormatMetadata(ExrFormat.Instance); + /// /// Gets the from .
diff --git a/src/ImageSharp/Formats/_Generated/_Formats.ttinclude b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude index 2d6129c4c0..c1c69c5b5b 100644 --- a/src/ImageSharp/Formats/_Generated/_Formats.ttinclude +++ b/src/ImageSharp/Formats/_Generated/_Formats.ttinclude @@ -14,7 +14,8 @@ "Qoi", "Tga", "Tiff", - "Webp" + "Webp", + "Exr" ]; private static readonly string[] frameFormats = [ diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 2c7172387f..32a5f0073e 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -56,6 +56,11 @@ True InlineArray.tt + + True + True + ImageExtensions.Save.tt + True True @@ -141,11 +146,6 @@ True PorterDuffFunctions.Generated.tt - - True - True - ImageExtensions.Save.tt - diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs index 620e305a21..165f13b07e 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs @@ -72,27 +72,10 @@ public Rgb96(uint r, uint g, uint b) [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) - { - 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); - } - /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public Vector4 ToVector4() => new( @@ -103,71 +86,48 @@ public void FromVector4(Vector4 vector) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromBgra5551(Bgra5551 source) => this.FromScaledVector4(source.ToScaledVector4()); + public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromArgb32(Argb32 source) => this.FromScaledVector4(source.ToScaledVector4()); + public bool Equals(Rgb96 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromBgr24(Bgr24 source) => this.FromScaledVector4(source.ToScaledVector4()); + public override string ToString() => FormattableString.Invariant($"Rgb96({this.R}, {this.G}, {this.B})"); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromBgra32(Bgra32 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static PixelOperations CreatePixelOperations() => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromL8(L8 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgb96 FromScaledVector4(Vector4 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromL16(L16 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgb96 FromVector4(Vector4 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromLa16(La16 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgb96 FromAbgr32(Abgr32 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromLa32(La32 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgb96 FromArgb32(Argb32 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromRgb24(Rgb24 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgb96 FromBgra5551(Bgra5551 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromRgba32(Rgba32 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgb96 FromBgr24(Bgr24 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromAbgr32(Abgr32 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgb96 FromBgra32(Bgra32 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ToRgba32(ref Rgba32 dest) => dest.FromScaledVector4(this.ToScaledVector4()); + public static Rgb96 FromL8(L8 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromRgb48(Rgb48 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgb96 FromL16(L16 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromRgba64(Rgba64 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgb96 FromLa16(La16 source) => throw new NotImplementedException(); - /// - public override bool Equals(object obj) => obj is Rgb96 other && this.Equals(other); + public static Rgb96 FromLa32(La32 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); + public static Rgb96 FromRgb24(Rgb24 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Equals(Rgb96 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); + public static Rgb96 FromRgba32(Rgba32 source) => throw new NotImplementedException(); - /// - public override string ToString() => FormattableString.Invariant($"Rgb96({this.R}, {this.G}, {this.B})"); + public static Rgb96 FromRgb48(Rgb48 source) => throw new NotImplementedException(); + + public static Rgb96 FromRgba64(Rgba64 source) => throw new NotImplementedException(); + + public static PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); + + public Rgba32 ToRgba32() => throw new NotImplementedException(); } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs index 912a62db0c..fd094be599 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs @@ -79,28 +79,6 @@ public Rgba128(uint r, uint g, uint b, uint a) [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( @@ -111,71 +89,50 @@ public void FromVector4(Vector4 vector) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromBgra5551(Bgra5551 source) => this.FromScaledVector4(source.ToScaledVector4()); + public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B, this.A); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromArgb32(Argb32 source) => this.FromScaledVector4(source.ToScaledVector4()); + 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); /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromBgr24(Bgr24 source) => this.FromScaledVector4(source.ToScaledVector4()); + public override string ToString() => FormattableString.Invariant($"Rgba128({this.R}, {this.G}, {this.B}, {this.A})"); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromBgra32(Bgra32 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static PixelOperations CreatePixelOperations() => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromL8(L8 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgba128 FromScaledVector4(Vector4 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromL16(L16 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgba128 FromVector4(Vector4 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromLa16(La16 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgba128 FromAbgr32(Abgr32 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromLa32(La32 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgba128 FromArgb32(Argb32 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromRgb24(Rgb24 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgba128 FromBgra5551(Bgra5551 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromRgba32(Rgba32 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgba128 FromBgr24(Bgr24 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromAbgr32(Abgr32 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgba128 FromBgra32(Bgra32 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void ToRgba32(ref Rgba32 dest) => dest.FromScaledVector4(this.ToScaledVector4()); + public static Rgba128 FromL8(L8 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromRgb48(Rgb48 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgba128 FromL16(L16 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void FromRgba64(Rgba64 source) => this.FromScaledVector4(source.ToScaledVector4()); + public static Rgba128 FromLa16(La16 source) => throw new NotImplementedException(); - /// - public override bool Equals(object obj) => obj is Rgba128 other && this.Equals(other); + public static Rgba128 FromLa32(La32 source) => throw new NotImplementedException(); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B, this.A); + public static Rgba128 FromRgb24(Rgb24 source) => throw new NotImplementedException(); - /// - [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 static Rgba128 FromRgba32(Rgba32 source) => throw new NotImplementedException(); - /// - public override string ToString() => FormattableString.Invariant($"Rgba128({this.R}, {this.G}, {this.B}, {this.A})"); + public static Rgba128 FromRgb48(Rgb48 source) => throw new NotImplementedException(); + + public static Rgba128 FromRgba64(Rgba64 source) => throw new NotImplementedException(); + + public static PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); + + public Rgba32 ToRgba32() => throw new NotImplementedException(); + + public Vector4 ToScaledVector4() => throw new NotImplementedException(); } diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index 29b75bc9cd..0391ab7c8c 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -10,14 +10,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Exr; [ValidateDisposedMemoryAllocations] public class ExrDecoderTests { - private static ExrDecoder ExrDecoder => new(); - [Theory] [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] public void ExrDecoder_CanDecode_Uncompressed(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using Image image = provider.GetImage(ExrDecoder); + using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); image.CompareToOriginal(provider); } @@ -27,7 +25,7 @@ public void ExrDecoder_CanDecode_Uncompressed(TestImageProvider public void ExrDecoder_CanDecode_ZipCompressed(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using Image image = provider.GetImage(ExrDecoder); + using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); image.CompareToOriginal(provider); } @@ -37,7 +35,7 @@ public void ExrDecoder_CanDecode_ZipCompressed(TestImageProvider public void ExrDecoder_CanDecode_ZipsCompressed(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using Image image = provider.GetImage(ExrDecoder); + using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); image.CompareToOriginal(provider); } @@ -47,7 +45,7 @@ public void ExrDecoder_CanDecode_ZipsCompressed(TestImageProvider(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using Image image = provider.GetImage(ExrDecoder); + using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); image.CompareToOriginal(provider); } @@ -57,7 +55,7 @@ public void ExrDecoder_CanDecode_RunLengthCompressed(TestImageProvider(TestImageProvider provider) where TPixel : unmanaged, IPixel { - using Image image = provider.GetImage(ExrDecoder); + using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); image.CompareToOriginal(provider); } diff --git a/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs index 926f5ca909..78ce845d36 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs @@ -11,56 +11,126 @@ namespace SixLabors.ImageSharp.Tests.Formats.Exr; public class ImageExtensionsTest { [Fact] - public void SaveAsExr_Stream() + public void SaveAsExr_Path() { - using var memoryStream = new MemoryStream(); + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsExr_Path.exr"); - using (var image = new Image(10, 10)) + using (Image image = new(10, 10)) { - image.SaveAsOpenExr(memoryStream, new ExrEncoder()); + image.SaveAsExr(file); } - memoryStream.Position = 0; + IImageFormat format = Image.DetectFormat(file); + Assert.True(format is ExrFormat); + } + + [Fact] + public async Task SaveAsExrAsync_Path() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensionsTest)); + string file = Path.Combine(dir, "SaveAsExrAsync_Path.exr"); - using (Image.Load(memoryStream, out IImageFormat mime)) + using (Image image = new(10, 10)) { - Assert.Equal("image/x-exr", mime.DefaultMimeType); + await image.SaveAsExrAsync(file); } + + IImageFormat format = Image.DetectFormat(file); + Assert.True(format is ExrFormat); } [Fact] - public void SaveAsExr_Stream_Encoder() + public void SaveAsExr_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsExr_Path_Encoder.exr"); + + using (Image image = new(10, 10)) + { + image.SaveAsExr(file, new ExrEncoder()); + } + + IImageFormat format = Image.DetectFormat(file); + Assert.True(format is ExrFormat); + } + + [Fact] + public async Task SaveAsExrAsync_Path_Encoder() + { + string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); + string file = Path.Combine(dir, "SaveAsExrAsync_Path_Encoder.tiff"); + + using (Image image = new(10, 10)) + { + await image.SaveAsExrAsync(file, new ExrEncoder()); + } + + IImageFormat format = Image.DetectFormat(file); + Assert.True(format is ExrFormat); + } + + [Fact] + public void SaveAsExr_Stream() { - using var memoryStream = new MemoryStream(); + using MemoryStream memoryStream = new(); - using (var image = new Image(10, 10)) + using (Image image = new(10, 10)) { - image.SaveAsOpenExr(memoryStream, new ExrEncoder()); + image.SaveAsExr(memoryStream); } memoryStream.Position = 0; - using (Image.Load(memoryStream, out IImageFormat mime)) + IImageFormat format = Image.DetectFormat(memoryStream); + Assert.True(format is ExrFormat); + } + + [Fact] + public async Task SaveAsExrAsync_StreamAsync() + { + using MemoryStream memoryStream = new(); + + using (Image image = new(10, 10)) { - Assert.Equal("image/x-exr", mime.DefaultMimeType); + await image.SaveAsExrAsync(memoryStream); } + + memoryStream.Position = 0; + + IImageFormat format = Image.DetectFormat(memoryStream); + Assert.True(format is ExrFormat); } [Fact] - public async Task SaveAsExrAsync_Stream_Encoder() + public void SaveAsExr_Stream_Encoder() { - using var memoryStream = new MemoryStream(); + using MemoryStream memoryStream = new(); - using (var image = new Image(10, 10)) + using (Image image = new(10, 10)) { - await image.SaveAsOpenExrAsync(memoryStream, new ExrEncoder()); + image.SaveAsTiff(memoryStream, new ExrEncoder()); } memoryStream.Position = 0; - using (Image.Load(memoryStream, out IImageFormat mime)) + IImageFormat format = Image.DetectFormat(memoryStream); + Assert.True(format is ExrFormat); + } + + [Fact] + public async Task SaveAsExrAsync_Stream_Encoder() + { + using MemoryStream memoryStream = new(); + + using (Image image = new(10, 10)) { - Assert.Equal("image/x-exr", mime.DefaultMimeType); + await image.SaveAsExrAsync(memoryStream, new ExrEncoder()); } + + memoryStream.Position = 0; + + IImageFormat format = Image.DetectFormat(memoryStream); + Assert.True(format is ExrFormat); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs index 4dc3def1f8..9ced51608b 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -64,7 +64,7 @@ private static Configuration CreateDefaultConfiguration() new TgaConfigurationModule(), new WebpConfigurationModule(), new TiffConfigurationModule(), - new ExrConfigurationModule()); + new ExrConfigurationModule(), new QoiConfigurationModule()); IImageEncoder pngEncoder = IsWindows ? SystemDrawingReferenceEncoder.Png : new ImageSharpPngEncoderWithDefaultConfiguration(); From 83dba5872b154716a8c23101395e6bd8be477a02 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 14 Mar 2026 18:10:06 +0100 Subject: [PATCH 21/66] Change exr name space to SixLabors.ImageSharp.Formats.Exr --- src/ImageSharp/Configuration.cs | 2 +- .../Compression/Decompressors/B44Compression.cs | 2 +- .../Compression/Decompressors/NoneExrCompression.cs | 2 +- .../Compression/Decompressors/RunLengthCompression.cs | 2 +- .../Compression/Decompressors/ZipExrCompression.cs | 2 +- .../{OpenExr => Exr}/Compression/ExrBaseCompression.cs | 2 +- .../{OpenExr => Exr}/Compression/ExrBaseDecompressor.cs | 2 +- .../{OpenExr => Exr}/Compression/ExrCompressionType.cs | 2 +- .../{OpenExr => Exr}/Compression/ExrDecompressorFactory.cs | 4 ++-- src/ImageSharp/Formats/{OpenExr => Exr}/ExrAttribute.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/ExrBox2i.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/ExrChannelInfo.cs | 2 +- .../Formats/{OpenExr => Exr}/ExrConfigurationModule.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/ExrConstants.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/ExrDecoder.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/ExrDecoderCore.cs | 4 ++-- src/ImageSharp/Formats/{OpenExr => Exr}/ExrDecoderOptions.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/ExrEncoder.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/ExrEncoderCore.cs | 4 ++-- src/ImageSharp/Formats/{OpenExr => Exr}/ExrFormat.cs | 2 +- .../Formats/{OpenExr => Exr}/ExrHeaderAttributes.cs | 4 ++-- src/ImageSharp/Formats/{OpenExr => Exr}/ExrImageDataType.cs | 2 +- .../Formats/{OpenExr => Exr}/ExrImageFormatDetector.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/ExrImageType.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/ExrLineOrder.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/ExrMetadata.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/ExrPixelType.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/ExrThrowHelper.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/IExrEncoderOptions.cs | 2 +- src/ImageSharp/Formats/{OpenExr => Exr}/README.md | 0 src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs | 2 +- src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs | 2 +- tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs | 2 +- tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs | 4 ++-- .../ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs | 2 +- 35 files changed, 39 insertions(+), 39 deletions(-) rename src/ImageSharp/Formats/{OpenExr => Exr}/Compression/Decompressors/B44Compression.cs (98%) rename src/ImageSharp/Formats/{OpenExr => Exr}/Compression/Decompressors/NoneExrCompression.cs (89%) rename src/ImageSharp/Formats/{OpenExr => Exr}/Compression/Decompressors/RunLengthCompression.cs (96%) rename src/ImageSharp/Formats/{OpenExr => Exr}/Compression/Decompressors/ZipExrCompression.cs (96%) rename src/ImageSharp/Formats/{OpenExr => Exr}/Compression/ExrBaseCompression.cs (93%) rename src/ImageSharp/Formats/{OpenExr => Exr}/Compression/ExrBaseDecompressor.cs (95%) rename src/ImageSharp/Formats/{OpenExr => Exr}/Compression/ExrCompressionType.cs (97%) rename src/ImageSharp/Formats/{OpenExr => Exr}/Compression/ExrDecompressorFactory.cs (90%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrAttribute.cs (92%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrBox2i.cs (91%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrChannelInfo.cs (94%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrConfigurationModule.cs (93%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrConstants.cs (97%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrDecoder.cs (96%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrDecoderCore.cs (99%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrDecoderOptions.cs (87%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrEncoder.cs (93%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrEncoderCore.cs (99%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrFormat.cs (94%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrHeaderAttributes.cs (93%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrImageDataType.cs (78%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrImageFormatDetector.cs (95%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrImageType.cs (74%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrLineOrder.cs (78%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrMetadata.cs (96%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrPixelType.cs (90%) rename src/ImageSharp/Formats/{OpenExr => Exr}/ExrThrowHelper.cs (95%) rename src/ImageSharp/Formats/{OpenExr => Exr}/IExrEncoderOptions.cs (87%) rename src/ImageSharp/Formats/{OpenExr => Exr}/README.md (100%) diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 7a2374fb6a..6e431ba31b 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -8,7 +8,7 @@ using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.OpenExr; +using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Qoi; diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/B44Compression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44Compression.cs similarity index 98% rename from src/ImageSharp/Formats/OpenExr/Compression/Decompressors/B44Compression.cs rename to src/ImageSharp/Formats/Exr/Compression/Decompressors/B44Compression.cs index 55e9003bc1..b8cb468142 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/B44Compression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44Compression.cs @@ -6,7 +6,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; internal class B44Compression : ExrBaseDecompressor { diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/NoneExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs similarity index 89% rename from src/ImageSharp/Formats/OpenExr/Compression/Decompressors/NoneExrCompression.cs rename to src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs index f5f16a0fd4..6b36dffa2b 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/NoneExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs @@ -4,7 +4,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; internal class NoneExrCompression : ExrBaseDecompressor { diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/RunLengthCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthCompression.cs similarity index 96% rename from src/ImageSharp/Formats/OpenExr/Compression/Decompressors/RunLengthCompression.cs rename to src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthCompression.cs index 0838dc1dcf..f1c7bb759b 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/RunLengthCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthCompression.cs @@ -5,7 +5,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; internal class RunLengthCompression : ExrBaseDecompressor { diff --git a/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs similarity index 96% rename from src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs rename to src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs index 1797bd44b3..4d9b42732f 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/Decompressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs @@ -7,7 +7,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; internal class ZipExrCompression : ExrBaseDecompressor { diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs similarity index 93% rename from src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs rename to src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs index 4fd5778cb6..3dab1006ed 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression; +namespace SixLabors.ImageSharp.Formats.Exr.Compression; internal abstract class ExrBaseCompression : IDisposable { diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs similarity index 95% rename from src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs rename to src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs index 2af814eb12..12edcc1046 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/ExrBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs @@ -4,7 +4,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression; +namespace SixLabors.ImageSharp.Formats.Exr.Compression; internal abstract class ExrBaseDecompressor : ExrBaseCompression { diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrCompressionType.cs b/src/ImageSharp/Formats/Exr/Compression/ExrCompressionType.cs similarity index 97% rename from src/ImageSharp/Formats/OpenExr/Compression/ExrCompressionType.cs rename to src/ImageSharp/Formats/Exr/Compression/ExrCompressionType.cs index 53fc2e2dc9..a7b584ed4c 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/ExrCompressionType.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrCompressionType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression; +namespace SixLabors.ImageSharp.Formats.Exr.Compression; internal enum ExrCompressionType { diff --git a/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs similarity index 90% rename from src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs rename to src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs index 538231548c..a293787fad 100644 --- a/src/ImageSharp/Formats/OpenExr/Compression/ExrDecompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs @@ -1,10 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.OpenExr.Compression.Decompressors; +using SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; using SixLabors.ImageSharp.Memory; -namespace SixLabors.ImageSharp.Formats.OpenExr.Compression; +namespace SixLabors.ImageSharp.Formats.Exr.Compression; internal static class ExrDecompressorFactory { diff --git a/src/ImageSharp/Formats/OpenExr/ExrAttribute.cs b/src/ImageSharp/Formats/Exr/ExrAttribute.cs similarity index 92% rename from src/ImageSharp/Formats/OpenExr/ExrAttribute.cs rename to src/ImageSharp/Formats/Exr/ExrAttribute.cs index 0d6ca86cc9..b4e95f1d47 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrAttribute.cs +++ b/src/ImageSharp/Formats/Exr/ExrAttribute.cs @@ -3,7 +3,7 @@ using System.Diagnostics; -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; [DebuggerDisplay("Name: {Name}, Type: {Type}, Length: {Length}")] internal class ExrAttribute diff --git a/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs b/src/ImageSharp/Formats/Exr/ExrBox2i.cs similarity index 91% rename from src/ImageSharp/Formats/OpenExr/ExrBox2i.cs rename to src/ImageSharp/Formats/Exr/ExrBox2i.cs index 1f5b0c0df8..61da9b36dc 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrBox2i.cs +++ b/src/ImageSharp/Formats/Exr/ExrBox2i.cs @@ -3,7 +3,7 @@ using System.Diagnostics; -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; [DebuggerDisplay("xMin: {XMin}, yMin: {YMin}, xMax: {XMax}, yMax: {YMax}")] internal struct ExrBox2i diff --git a/src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs b/src/ImageSharp/Formats/Exr/ExrChannelInfo.cs similarity index 94% rename from src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs rename to src/ImageSharp/Formats/Exr/ExrChannelInfo.cs index d39bc08a5a..915b8201d9 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrChannelInfo.cs +++ b/src/ImageSharp/Formats/Exr/ExrChannelInfo.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; [DebuggerDisplay("Name: {ChannelName}, PixelType: {PixelType}")] [StructLayout(LayoutKind.Sequential, Pack = 1)] diff --git a/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs b/src/ImageSharp/Formats/Exr/ExrConfigurationModule.cs similarity index 93% rename from src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs rename to src/ImageSharp/Formats/Exr/ExrConfigurationModule.cs index c0322728aa..a2f1b8c7ab 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrConfigurationModule.cs +++ b/src/ImageSharp/Formats/Exr/ExrConfigurationModule.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; /// /// Registers the image encoders, decoders and mime type detectors for the OpenExr format. diff --git a/src/ImageSharp/Formats/OpenExr/ExrConstants.cs b/src/ImageSharp/Formats/Exr/ExrConstants.cs similarity index 97% rename from src/ImageSharp/Formats/OpenExr/ExrConstants.cs rename to src/ImageSharp/Formats/Exr/ExrConstants.cs index 42ccf27fbd..214dbd8321 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrConstants.cs +++ b/src/ImageSharp/Formats/Exr/ExrConstants.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; /// /// Defines constants relating to OpenExr images. diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoder.cs b/src/ImageSharp/Formats/Exr/ExrDecoder.cs similarity index 96% rename from src/ImageSharp/Formats/OpenExr/ExrDecoder.cs rename to src/ImageSharp/Formats/Exr/ExrDecoder.cs index 84d441eb72..11937eed4f 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoder.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoder.cs @@ -3,7 +3,7 @@ using SixLabors.ImageSharp.PixelFormats; -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; /// /// Image decoder for generating an image out of a OpenExr stream. diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs similarity index 99% rename from src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs rename to src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 6eaf56fde3..732b4ff4a7 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -6,13 +6,13 @@ using System.Buffers.Binary; using System.Runtime.CompilerServices; using System.Text; -using SixLabors.ImageSharp.Formats.OpenExr.Compression; +using SixLabors.ImageSharp.Formats.Exr.Compression; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; /// /// Performs the OpenExr decoding operation. diff --git a/src/ImageSharp/Formats/OpenExr/ExrDecoderOptions.cs b/src/ImageSharp/Formats/Exr/ExrDecoderOptions.cs similarity index 87% rename from src/ImageSharp/Formats/OpenExr/ExrDecoderOptions.cs rename to src/ImageSharp/Formats/Exr/ExrDecoderOptions.cs index c0b419b937..a8cda0b357 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrDecoderOptions.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderOptions.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; /// /// Image decoder options for decoding OpenExr streams. diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoder.cs b/src/ImageSharp/Formats/Exr/ExrEncoder.cs similarity index 93% rename from src/ImageSharp/Formats/OpenExr/ExrEncoder.cs rename to src/ImageSharp/Formats/Exr/ExrEncoder.cs index 9960efd686..95913f2beb 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoder.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoder.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; /// /// Image encoder for writing an image to a stream in the OpenExr Format. diff --git a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs similarity index 99% rename from src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs rename to src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index 740b71e12b..98f43f6e27 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -6,12 +6,12 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Threading.Channels; -using SixLabors.ImageSharp.Formats.OpenExr.Compression; +using SixLabors.ImageSharp.Formats.Exr.Compression; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; /// /// Image encoder for writing an image to a stream in the OpenExr format. diff --git a/src/ImageSharp/Formats/OpenExr/ExrFormat.cs b/src/ImageSharp/Formats/Exr/ExrFormat.cs similarity index 94% rename from src/ImageSharp/Formats/OpenExr/ExrFormat.cs rename to src/ImageSharp/Formats/Exr/ExrFormat.cs index d367977fc6..4415c15b06 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrFormat.cs +++ b/src/ImageSharp/Formats/Exr/ExrFormat.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; /// /// Registers the image encoders, decoders and mime type detectors for the OpenExr format. diff --git a/src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs b/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs similarity index 93% rename from src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs rename to src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs index 4cec4fbc58..29c30e7935 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrHeaderAttributes.cs +++ b/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.OpenExr.Compression; +using SixLabors.ImageSharp.Formats.Exr.Compression; -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; internal class ExrHeaderAttributes { diff --git a/src/ImageSharp/Formats/OpenExr/ExrImageDataType.cs b/src/ImageSharp/Formats/Exr/ExrImageDataType.cs similarity index 78% rename from src/ImageSharp/Formats/OpenExr/ExrImageDataType.cs rename to src/ImageSharp/Formats/Exr/ExrImageDataType.cs index 4c3e19af81..92ca0f771b 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrImageDataType.cs +++ b/src/ImageSharp/Formats/Exr/ExrImageDataType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; internal enum ExrImageDataType { diff --git a/src/ImageSharp/Formats/OpenExr/ExrImageFormatDetector.cs b/src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs similarity index 95% rename from src/ImageSharp/Formats/OpenExr/ExrImageFormatDetector.cs rename to src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs index 2f97e74048..29548d52ef 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs @@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.Formats.Qoi; -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; /// /// Detects OpenExr file headers. diff --git a/src/ImageSharp/Formats/OpenExr/ExrImageType.cs b/src/ImageSharp/Formats/Exr/ExrImageType.cs similarity index 74% rename from src/ImageSharp/Formats/OpenExr/ExrImageType.cs rename to src/ImageSharp/Formats/Exr/ExrImageType.cs index a59aadbd62..bae5e21777 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrImageType.cs +++ b/src/ImageSharp/Formats/Exr/ExrImageType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; internal enum ExrImageType { diff --git a/src/ImageSharp/Formats/OpenExr/ExrLineOrder.cs b/src/ImageSharp/Formats/Exr/ExrLineOrder.cs similarity index 78% rename from src/ImageSharp/Formats/OpenExr/ExrLineOrder.cs rename to src/ImageSharp/Formats/Exr/ExrLineOrder.cs index dcf1c37c9e..8b9a6d7722 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrLineOrder.cs +++ b/src/ImageSharp/Formats/Exr/ExrLineOrder.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; internal enum ExrLineOrder : byte { diff --git a/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs b/src/ImageSharp/Formats/Exr/ExrMetadata.cs similarity index 96% rename from src/ImageSharp/Formats/OpenExr/ExrMetadata.cs rename to src/ImageSharp/Formats/Exr/ExrMetadata.cs index 9cb58e9e67..2e83826c96 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrMetadata.cs +++ b/src/ImageSharp/Formats/Exr/ExrMetadata.cs @@ -4,7 +4,7 @@ using System.Numerics; using SixLabors.ImageSharp.PixelFormats; -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; /// /// Provides OpenExr specific metadata information for the image. diff --git a/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs b/src/ImageSharp/Formats/Exr/ExrPixelType.cs similarity index 90% rename from src/ImageSharp/Formats/OpenExr/ExrPixelType.cs rename to src/ImageSharp/Formats/Exr/ExrPixelType.cs index 0dd49eaf8f..7016063ecc 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrPixelType.cs +++ b/src/ImageSharp/Formats/Exr/ExrPixelType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; /// /// The different pixel formats for a OpenEXR image. diff --git a/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs b/src/ImageSharp/Formats/Exr/ExrThrowHelper.cs similarity index 95% rename from src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs rename to src/ImageSharp/Formats/Exr/ExrThrowHelper.cs index b609d0038c..f668dcfefd 100644 --- a/src/ImageSharp/Formats/OpenExr/ExrThrowHelper.cs +++ b/src/ImageSharp/Formats/Exr/ExrThrowHelper.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; /// /// Cold path optimizations for throwing exr format based exceptions. diff --git a/src/ImageSharp/Formats/OpenExr/IExrEncoderOptions.cs b/src/ImageSharp/Formats/Exr/IExrEncoderOptions.cs similarity index 87% rename from src/ImageSharp/Formats/OpenExr/IExrEncoderOptions.cs rename to src/ImageSharp/Formats/Exr/IExrEncoderOptions.cs index ba2db62a00..0d90514e9c 100644 --- a/src/ImageSharp/Formats/OpenExr/IExrEncoderOptions.cs +++ b/src/ImageSharp/Formats/Exr/IExrEncoderOptions.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.OpenExr; +namespace SixLabors.ImageSharp.Formats.Exr; /// /// Configuration options for use during OpenExr encoding. diff --git a/src/ImageSharp/Formats/OpenExr/README.md b/src/ImageSharp/Formats/Exr/README.md similarity index 100% rename from src/ImageSharp/Formats/OpenExr/README.md rename to src/ImageSharp/Formats/Exr/README.md diff --git a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs index ca4ad0e06c..fa6eaa722d 100644 --- a/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/_Generated/ImageExtensions.Save.cs @@ -13,7 +13,7 @@ using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Webp; -using SixLabors.ImageSharp.Formats.OpenExr; +using SixLabors.ImageSharp.Formats.Exr; namespace SixLabors.ImageSharp; diff --git a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs index 31a25fcede..0dcbaed808 100644 --- a/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs +++ b/src/ImageSharp/Formats/_Generated/ImageMetadataExtensions.cs @@ -14,7 +14,7 @@ using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Webp; -using SixLabors.ImageSharp.Formats.OpenExr; +using SixLabors.ImageSharp.Formats.Exr; namespace SixLabors.ImageSharp; diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index 0391ab7c8c..846dd42ca9 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.OpenExr; +using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests.Formats.Exr; diff --git a/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs index 78ce845d36..a093ef4c5b 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats; -using SixLabors.ImageSharp.Formats.OpenExr; +using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests.Formats.Exr; @@ -109,7 +109,7 @@ public void SaveAsExr_Stream_Encoder() using (Image image = new(10, 10)) { - image.SaveAsTiff(memoryStream, new ExrEncoder()); + image.SaveAsExr(memoryStream, new ExrEncoder()); } memoryStream.Position = 0; diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs index 9ced51608b..8a40bd6720 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -3,9 +3,9 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Exr; 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.Qoi; From b78402d5242a3024635c67bd82d43f238569284a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 14 Mar 2026 19:18:30 +0100 Subject: [PATCH 22/66] Fix issue in ExrImageFormatDetector.cs returning wrong format --- src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs | 3 +-- tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs b/src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs index 29548d52ef..62663a4a78 100644 --- a/src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs +++ b/src/ImageSharp/Formats/Exr/ExrImageFormatDetector.cs @@ -3,7 +3,6 @@ using System.Buffers.Binary; using System.Diagnostics.CodeAnalysis; -using SixLabors.ImageSharp.Formats.Qoi; namespace SixLabors.ImageSharp.Formats.Exr; @@ -29,7 +28,7 @@ private bool IsSupportedFileFormat(ReadOnlySpan header) /// public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) { - format = this.IsSupportedFileFormat(header) ? QoiFormat.Instance : null; + format = this.IsSupportedFileFormat(header) ? ExrFormat.Instance : null; return format != null; } } diff --git a/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs index a093ef4c5b..cd23e1ccd0 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ImageExtensionsTest.cs @@ -59,7 +59,7 @@ public void SaveAsExr_Path_Encoder() public async Task SaveAsExrAsync_Path_Encoder() { string dir = TestEnvironment.CreateOutputDirectory(nameof(ImageExtensions)); - string file = Path.Combine(dir, "SaveAsExrAsync_Path_Encoder.tiff"); + string file = Path.Combine(dir, "SaveAsExrAsync_Path_Encoder.exr"); using (Image image = new(10, 10)) { From faf87fbe16066a583aac493cad7339666cefec08 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 14 Mar 2026 19:37:39 +0100 Subject: [PATCH 23/66] Use MagickReference decoder for exr decoding tests --- .../ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs | 13 ++++++++----- .../ReferenceCodecs/MagickReferenceDecoder.cs | 4 ++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index 846dd42ca9..c90d4bdbad 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; namespace SixLabors.ImageSharp.Tests.Formats.Exr; @@ -10,6 +11,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Exr; [ValidateDisposedMemoryAllocations] public class ExrDecoderTests { + private static MagickReferenceDecoder ReferenceDecoder => MagickReferenceDecoder.Exr; + [Theory] [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] public void ExrDecoder_CanDecode_Uncompressed(TestImageProvider provider) @@ -17,7 +20,7 @@ public void ExrDecoder_CanDecode_Uncompressed(TestImageProvider { using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider); + image.CompareToOriginal(provider, ReferenceDecoder); } [Theory] @@ -27,7 +30,7 @@ public void ExrDecoder_CanDecode_ZipCompressed(TestImageProvider { using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider); + image.CompareToOriginal(provider, ReferenceDecoder); } [Theory] @@ -37,7 +40,7 @@ public void ExrDecoder_CanDecode_ZipsCompressed(TestImageProvider image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider); + image.CompareToOriginal(provider, ReferenceDecoder); } [Theory] @@ -47,7 +50,7 @@ public void ExrDecoder_CanDecode_RunLengthCompressed(TestImageProvider image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider); + image.CompareToOriginal(provider, ReferenceDecoder); } [Theory] @@ -57,6 +60,6 @@ public void ExrDecoder_CanDecode_B44Compressed(TestImageProvider { using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider); + image.CompareToOriginal(provider, ReferenceDecoder); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 862d4b64d3..a63818562f 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -6,6 +6,7 @@ using ImageMagick.Formats; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Tiff; @@ -42,6 +43,8 @@ public MagickReferenceDecoder(IImageFormat imageFormat, bool validate) public static MagickReferenceDecoder WebP { get; } = new(WebpFormat.Instance); + public static MagickReferenceDecoder Exr { get; } = new(ExrFormat.Instance); + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { ImageMetadata metadata = new(); @@ -118,6 +121,7 @@ protected override ImageInfo Identify(DecoderOptions options, Stream stream, Can PixelType = metadata.GetDecodedPixelTypeInfo() }; } + private static void FromRgba32Bytes( Configuration configuration, Span rgbaBytes, From eaea042bc369dcaee361c00670f14df02cfe6ae9 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 15 Mar 2026 15:15:21 +0100 Subject: [PATCH 24/66] Fix issue assigning pixel value from HalfVector4 --- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 732b4ff4a7..7d74b54251 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -159,8 +159,7 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf for (int x = 0; x < width; x++) { HalfVector4 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1.0f); - TPixel.FromVector4(pixelValue.ToVector4()); - pixelRow[x] = color; + pixelRow[x] = TPixel.FromVector4(pixelValue.ToVector4()); } } From 490d4a0c2db8710ba24b9b1c353125c5eb3b005b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 15 Mar 2026 16:56:04 +0100 Subject: [PATCH 25/66] Fix mistake in Identify --- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 7d74b54251..533cff2fc9 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -75,10 +75,19 @@ public ExrDecoderCore(ExrDecoderOptions options) /// private ExrCompressionType Compression { get; set; } + /// + /// Gets or sets the image data type, either RGB, RGBA or gray. + /// private ExrImageDataType ImageDataType { get; set; } + /// + /// Gets or sets the image type, either ScanLine or tiled. + /// private ExrImageType ImageType { get; set; } + /// + /// Gets or sets the header attributes. + /// private ExrHeaderAttributes HeaderAttributes { get; set; } /// @@ -134,7 +143,6 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock, width, height, rowsPerBlock, channelCount); - TPixel color = default; for (uint y = 0; y < height; y += rowsPerBlock) { ulong rowOffset = this.ReadUnsignedLong(stream); @@ -368,7 +376,7 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok int bitsPerPixel = this.CalculateBitsPerPixel(); - return new ImageInfo(new Size((int)header.ScreenWindowWidth, (int)header.AspectRatio), this.metadata); + return new ImageInfo(new Size(header.DataWindow.XMax, header.DataWindow.YMax), this.metadata); } private int CalculateBitsPerPixel() From d12103995699ff176e66ff575df5cba9a2ae5436 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 15 Mar 2026 16:56:24 +0100 Subject: [PATCH 26/66] Add test for Identify --- .../ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index c90d4bdbad..0f039f7fb0 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -13,6 +13,18 @@ public class ExrDecoderTests { private static MagickReferenceDecoder ReferenceDecoder => MagickReferenceDecoder.Exr; + [Theory] + [InlineData(TestImages.Exr.Uncompressed, 803, 1197)] + public void ExrDecoder_Identify_DetectsCorrectWidthAndHeight(string imagePath, int expectedWidth, int expectedHeight) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + Assert.NotNull(imageInfo); + Assert.Equal(expectedWidth, imageInfo.Width); + Assert.Equal(expectedHeight, imageInfo.Height); + } + [Theory] [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] public void ExrDecoder_CanDecode_Uncompressed(TestImageProvider provider) From 1b97d8c12ce786ca4f2ba297d0c505facb362ae4 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 15 Mar 2026 19:39:17 +0100 Subject: [PATCH 27/66] Smaller test files --- tests/Images/Input/Exr/Calliphora_b44.exr | 4 ++-- tests/Images/Input/Exr/Calliphora_rle.exr | 4 ++-- tests/Images/Input/Exr/Calliphora_uncompressed.exr | 4 ++-- tests/Images/Input/Exr/Calliphora_zip.exr | 4 ++-- tests/Images/Input/Exr/Calliphora_zips.exr | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/Images/Input/Exr/Calliphora_b44.exr b/tests/Images/Input/Exr/Calliphora_b44.exr index ebe464170f..ea008e0701 100644 --- a/tests/Images/Input/Exr/Calliphora_b44.exr +++ b/tests/Images/Input/Exr/Calliphora_b44.exr @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cde761051241cba1e7f8466d71602abdb130677de55602fe6afec4b287c75b9d -size 2533521 +oid sha256:7a6ac7fd879fc2dae646adaa14382aa10e5b8d02634af4668dd04bce09148151 +size 157973 diff --git a/tests/Images/Input/Exr/Calliphora_rle.exr b/tests/Images/Input/Exr/Calliphora_rle.exr index 20878f8896..a956c9c400 100644 --- a/tests/Images/Input/Exr/Calliphora_rle.exr +++ b/tests/Images/Input/Exr/Calliphora_rle.exr @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6302e72676f5574b8a9dde7e4385cff1c115e9833b630118328b88aea07e31d0 -size 4098454 +oid sha256:ccd25a952240a75d9c8b646be5001a2b006c053314e30cdf12e35ac865a31ae2 +size 292545 diff --git a/tests/Images/Input/Exr/Calliphora_uncompressed.exr b/tests/Images/Input/Exr/Calliphora_uncompressed.exr index f0003a1f91..5dc90343db 100644 --- a/tests/Images/Input/Exr/Calliphora_uncompressed.exr +++ b/tests/Images/Input/Exr/Calliphora_uncompressed.exr @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71613ff5972fa9e7b405ac944d159d1791ad229f5560da616438c9d718eafd24 -size 5798633 +oid sha256:f54e5e57df1b8cdf1b26418d5696dd9cefbd7ed3bf31cdcde06fd9a7bf5e3724 +size 362681 diff --git a/tests/Images/Input/Exr/Calliphora_zip.exr b/tests/Images/Input/Exr/Calliphora_zip.exr index 5d17150a49..82dc0dc39f 100644 --- a/tests/Images/Input/Exr/Calliphora_zip.exr +++ b/tests/Images/Input/Exr/Calliphora_zip.exr @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92c78a7cf9fce29e725511acadf49f0f784914da5bef55b0e0c4e75ba09c3a75 -size 2652397 +oid sha256:a8b8e3b9a74179cb51a7a85031b3d177e109c20a56392804b10873273ff2163e +size 181871 diff --git a/tests/Images/Input/Exr/Calliphora_zips.exr b/tests/Images/Input/Exr/Calliphora_zips.exr index b4b2976822..6556adf00f 100644 --- a/tests/Images/Input/Exr/Calliphora_zips.exr +++ b/tests/Images/Input/Exr/Calliphora_zips.exr @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e34a9b0bb7fd6eb24eef633dea737653812fd8ab5c5352f6e10523055823b1d -size 2918571 +oid sha256:9c6962a5b06c677648d5e22ff30ba8a28e7801fffb32259984e23bbaa97061b1 +size 220159 From e4c22d8f6725efd8b44492889f87a1b8ec1a587d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 15 Mar 2026 19:55:53 +0100 Subject: [PATCH 28/66] Add tests for decode RGB and gray exr files --- .../Formats/Exr/ExrDecoderTests.cs | 24 +++++++++++++++++-- tests/ImageSharp.Tests/TestImages.cs | 2 ++ tests/Images/Input/Exr/Calliphora_gray.exr | 3 +++ tests/Images/Input/Exr/Calliphora_rgb.exr | 3 +++ 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 tests/Images/Input/Exr/Calliphora_gray.exr create mode 100644 tests/Images/Input/Exr/Calliphora_rgb.exr diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index 0f039f7fb0..47e9b32711 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -14,7 +14,7 @@ public class ExrDecoderTests private static MagickReferenceDecoder ReferenceDecoder => MagickReferenceDecoder.Exr; [Theory] - [InlineData(TestImages.Exr.Uncompressed, 803, 1197)] + [InlineData(TestImages.Exr.Uncompressed, 199, 297)] public void ExrDecoder_Identify_DetectsCorrectWidthAndHeight(string imagePath, int expectedWidth, int expectedHeight) { TestFile testFile = TestFile.Create(imagePath); @@ -27,7 +27,27 @@ public void ExrDecoder_Identify_DetectsCorrectWidthAndHeight(string imag [Theory] [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] - public void ExrDecoder_CanDecode_Uncompressed(TestImageProvider provider) + public void ExrDecoder_CanDecode_Uncompressed_RGBA(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + + [Theory] + [WithFile(TestImages.Exr.Rgb, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Rgb(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + + [Theory] + [WithFile(TestImages.Exr.Gray, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Gray(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(ExrDecoder.Instance); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 2429c05d2b..06e35728e4 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1388,5 +1388,7 @@ public static class Exr public const string Zips = "Exr/Calliphora_zips.exr"; public const string Rle = "Exr/Calliphora_rle.exr"; public const string B44 = "Exr/Calliphora_b44.exr"; + public const string Rgb = "Exr/Calliphora_rgb.exr"; + public const string Gray = "Exr/Calliphora_gray.exr"; } } diff --git a/tests/Images/Input/Exr/Calliphora_gray.exr b/tests/Images/Input/Exr/Calliphora_gray.exr new file mode 100644 index 0000000000..2aaeafbbe8 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_gray.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a5daadcfd4ff0e45282d39d6c54f9a13651da3fd8841abda580e76661555470 +size 124245 diff --git a/tests/Images/Input/Exr/Calliphora_rgb.exr b/tests/Images/Input/Exr/Calliphora_rgb.exr new file mode 100644 index 0000000000..5dc90343db --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_rgb.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f54e5e57df1b8cdf1b26418d5696dd9cefbd7ed3bf31cdcde06fd9a7bf5e3724 +size 362681 From 1abfe6966dd39a29bc40a2552604d49f96d0d460 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 16 Mar 2026 15:43:41 +0100 Subject: [PATCH 29/66] Add test image for decoding exr with float piyel type --- .../ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs | 12 +++++++++++- tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Exr/rgb_float32_uncompressed.exr | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 tests/Images/Input/Exr/rgb_float32_uncompressed.exr diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index 47e9b32711..9f1a532acc 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -27,7 +27,17 @@ public void ExrDecoder_Identify_DetectsCorrectWidthAndHeight(string imag [Theory] [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] - public void ExrDecoder_CanDecode_Uncompressed_RGBA(TestImageProvider provider) + public void ExrDecoder_CanDecode_Uncompressed_Rgba_ExrPixelType_Half(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + + [Theory] + [WithFile(TestImages.Exr.UncompressedFloatRgb, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Float(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(ExrDecoder.Instance); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 06e35728e4..6d5302a9bc 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1390,5 +1390,6 @@ public static class Exr public const string B44 = "Exr/Calliphora_b44.exr"; public const string Rgb = "Exr/Calliphora_rgb.exr"; public const string Gray = "Exr/Calliphora_gray.exr"; + public const string UncompressedFloatRgb = "Exr/rgb_float32_uncompressed.exr"; } } diff --git a/tests/Images/Input/Exr/rgb_float32_uncompressed.exr b/tests/Images/Input/Exr/rgb_float32_uncompressed.exr new file mode 100644 index 0000000000..8390e0f8ee --- /dev/null +++ b/tests/Images/Input/Exr/rgb_float32_uncompressed.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:67b2d2f5fdacbe5cb2341fe230737fde041321224cebd65d5d12245a4f02d554 +size 388342 From fc40209727ada7d5ee638ea06b0753cf4a97d089 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 16 Mar 2026 19:30:09 +0100 Subject: [PATCH 30/66] Test case for pixel type uint --- tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs | 10 ++++++++++ tests/ImageSharp.Tests/TestImages.cs | 3 ++- tests/Images/Input/Exr/rgb_uint32_uncompressed.exr | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 tests/Images/Input/Exr/rgb_uint32_uncompressed.exr diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index 9f1a532acc..beca934484 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -45,6 +45,16 @@ public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Float(Tes image.CompareToOriginal(provider, ReferenceDecoder); } + [Theory] + [WithFile(TestImages.Exr.UncompressedUintRgb, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + [Theory] [WithFile(TestImages.Exr.Rgb, PixelTypes.Rgba32)] public void ExrDecoder_CanDecode_Rgb(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 6d5302a9bc..1b0c0c865b 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1384,12 +1384,13 @@ public static class Cur public static class Exr { public const string Uncompressed = "Exr/Calliphora_uncompressed.exr"; + public const string UncompressedFloatRgb = "Exr/rgb_float32_uncompressed.exr"; + public const string UncompressedUintRgb = "Exr/rgb_uint32_uncompressed.exr"; public const string Zip = "Exr/Calliphora_zip.exr"; public const string Zips = "Exr/Calliphora_zips.exr"; public const string Rle = "Exr/Calliphora_rle.exr"; public const string B44 = "Exr/Calliphora_b44.exr"; public const string Rgb = "Exr/Calliphora_rgb.exr"; public const string Gray = "Exr/Calliphora_gray.exr"; - public const string UncompressedFloatRgb = "Exr/rgb_float32_uncompressed.exr"; } } diff --git a/tests/Images/Input/Exr/rgb_uint32_uncompressed.exr b/tests/Images/Input/Exr/rgb_uint32_uncompressed.exr new file mode 100644 index 0000000000..616b676c3c --- /dev/null +++ b/tests/Images/Input/Exr/rgb_uint32_uncompressed.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8de6e6f5b1c5acfc67d84e228c45f47ec16a1635f90fd484dfaeb89c28278f10 +size 101408 From a0eaefbae95158d28b0eeea7d84f81e891516212 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 17 Mar 2026 09:50:16 +0100 Subject: [PATCH 31/66] Try fix decode unsigned int pixel data --- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 15 +++++++++++---- .../Images/Input/Exr/rgb_uint32_uncompressed.exr | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 533cff2fc9..b823f452ee 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -4,7 +4,9 @@ using System.Buffers; using System.Buffers.Binary; +using System.Numerics; using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; using System.Text; using SixLabors.ImageSharp.Formats.Exr.Compression; using SixLabors.ImageSharp.IO; @@ -19,6 +21,9 @@ namespace SixLabors.ImageSharp.Formats.Exr; /// internal sealed class ExrDecoderCore : ImageDecoderCore { + private const float Scale32Bit = 1f / 0xFFFFFFFF; + private static readonly Vector4 Scale32BitVector = Vector128.Create(Scale32Bit, Scale32Bit, Scale32Bit, 1f).AsVector4(); + /// /// Reusable buffer. /// @@ -196,7 +201,6 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock, width, height, rowsPerBlock, channelCount); - TPixel color = default; for (uint y = 0; y < height; y += rowsPerBlock) { ulong rowOffset = this.ReadUnsignedLong(stream); @@ -222,9 +226,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe for (int x = 0; x < width; x++) { - Rgba128 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue); - TPixel.FromVector4(pixelValue.ToVector4()); - pixelRow[x] = color; + pixelRow[x] = ColorScaleTo32Bit(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue); } } } @@ -799,4 +801,9 @@ private int ReadSignedInteger(BufferedReadStream stream) return BinaryPrimitives.ReadInt32LittleEndian(this.buffer); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TPixel ColorScaleTo32Bit(uint r, uint g, uint b, uint a) + where TPixel : unmanaged, IPixel + => TPixel.FromScaledVector4(new Vector4(r, g, b, a) * Scale32Bit); } diff --git a/tests/Images/Input/Exr/rgb_uint32_uncompressed.exr b/tests/Images/Input/Exr/rgb_uint32_uncompressed.exr index 616b676c3c..9a652749f2 100644 --- a/tests/Images/Input/Exr/rgb_uint32_uncompressed.exr +++ b/tests/Images/Input/Exr/rgb_uint32_uncompressed.exr @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8de6e6f5b1c5acfc67d84e228c45f47ec16a1635f90fd484dfaeb89c28278f10 -size 101408 +oid sha256:487f4641908b0c5c34e7cc3be439eff31e5e8d2fd03a9d17d39dc3dc5e55874d +size 243558 From b79ba2158d44e1bc9176e671519ce4c8cb60eaa9 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 17 Mar 2026 10:31:50 +0100 Subject: [PATCH 32/66] Fix not using offset when accessing decompressedPixelData --- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index b823f452ee..c19436d523 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -219,7 +219,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { ExrChannelInfo channel = this.Channels[channelIdx]; - offset += this.ReadUnsignedIntChannelData(stream, channel, decompressedPixelData, redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); + offset += this.ReadUnsignedIntChannelData(stream, channel, decompressedPixelData.Slice(offset), redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); } stream.Position = nextRowOffsetPosition; From 51f3ab2526b1691a67a6a8515cc8d81972559df4 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 17 Mar 2026 13:36:29 +0100 Subject: [PATCH 33/66] Fix test image for float32: it is now actually uncompressed --- tests/Images/Input/Exr/rgb_float32_uncompressed.exr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Images/Input/Exr/rgb_float32_uncompressed.exr b/tests/Images/Input/Exr/rgb_float32_uncompressed.exr index 8390e0f8ee..489758bb04 100644 --- a/tests/Images/Input/Exr/rgb_float32_uncompressed.exr +++ b/tests/Images/Input/Exr/rgb_float32_uncompressed.exr @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:67b2d2f5fdacbe5cb2341fe230737fde041321224cebd65d5d12245a4f02d554 -size 388342 +oid sha256:5ab8b71824ca384fc2cde1f74a6f34c38169fa328ccc67f17d08333e9de42f55 +size 243558 From 3b7aa4d55469e29c6dacaddf09e6bf9aa25b723d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 19 Mar 2026 15:30:04 +0100 Subject: [PATCH 34/66] Move constants to own folder --- src/ImageSharp/Formats/Exr/{ => Constants}/ExrImageDataType.cs | 2 +- src/ImageSharp/Formats/Exr/{ => Constants}/ExrImageType.cs | 2 +- src/ImageSharp/Formats/Exr/{ => Constants}/ExrLineOrder.cs | 2 +- src/ImageSharp/Formats/Exr/{ => Constants}/ExrPixelType.cs | 2 +- src/ImageSharp/Formats/Exr/ExrChannelInfo.cs | 1 + src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 1 + src/ImageSharp/Formats/Exr/ExrEncoder.cs | 2 ++ src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 1 + src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs | 1 + src/ImageSharp/Formats/Exr/ExrMetadata.cs | 1 + src/ImageSharp/Formats/Exr/IExrEncoderOptions.cs | 2 ++ 11 files changed, 13 insertions(+), 4 deletions(-) rename src/ImageSharp/Formats/Exr/{ => Constants}/ExrImageDataType.cs (76%) rename src/ImageSharp/Formats/Exr/{ => Constants}/ExrImageType.cs (72%) rename src/ImageSharp/Formats/Exr/{ => Constants}/ExrLineOrder.cs (76%) rename src/ImageSharp/Formats/Exr/{ => Constants}/ExrPixelType.cs (89%) diff --git a/src/ImageSharp/Formats/Exr/ExrImageDataType.cs b/src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs similarity index 76% rename from src/ImageSharp/Formats/Exr/ExrImageDataType.cs rename to src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs index 92ca0f771b..d63a8e2198 100644 --- a/src/ImageSharp/Formats/Exr/ExrImageDataType.cs +++ b/src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Exr; +namespace SixLabors.ImageSharp.Formats.Exr.Constants; internal enum ExrImageDataType { diff --git a/src/ImageSharp/Formats/Exr/ExrImageType.cs b/src/ImageSharp/Formats/Exr/Constants/ExrImageType.cs similarity index 72% rename from src/ImageSharp/Formats/Exr/ExrImageType.cs rename to src/ImageSharp/Formats/Exr/Constants/ExrImageType.cs index bae5e21777..beeabe35e1 100644 --- a/src/ImageSharp/Formats/Exr/ExrImageType.cs +++ b/src/ImageSharp/Formats/Exr/Constants/ExrImageType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Exr; +namespace SixLabors.ImageSharp.Formats.Exr.Constants; internal enum ExrImageType { diff --git a/src/ImageSharp/Formats/Exr/ExrLineOrder.cs b/src/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs similarity index 76% rename from src/ImageSharp/Formats/Exr/ExrLineOrder.cs rename to src/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs index 8b9a6d7722..56573472ca 100644 --- a/src/ImageSharp/Formats/Exr/ExrLineOrder.cs +++ b/src/ImageSharp/Formats/Exr/Constants/ExrLineOrder.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Exr; +namespace SixLabors.ImageSharp.Formats.Exr.Constants; internal enum ExrLineOrder : byte { diff --git a/src/ImageSharp/Formats/Exr/ExrPixelType.cs b/src/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs similarity index 89% rename from src/ImageSharp/Formats/Exr/ExrPixelType.cs rename to src/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs index 7016063ecc..cddc43d594 100644 --- a/src/ImageSharp/Formats/Exr/ExrPixelType.cs +++ b/src/ImageSharp/Formats/Exr/Constants/ExrPixelType.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Exr; +namespace SixLabors.ImageSharp.Formats.Exr.Constants; /// /// The different pixel formats for a OpenEXR image. diff --git a/src/ImageSharp/Formats/Exr/ExrChannelInfo.cs b/src/ImageSharp/Formats/Exr/ExrChannelInfo.cs index 915b8201d9..d4f5825b99 100644 --- a/src/ImageSharp/Formats/Exr/ExrChannelInfo.cs +++ b/src/ImageSharp/Formats/Exr/ExrChannelInfo.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Exr.Constants; namespace SixLabors.ImageSharp.Formats.Exr; diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index c19436d523..38e9c3ec90 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -9,6 +9,7 @@ using System.Runtime.Intrinsics; using System.Text; using SixLabors.ImageSharp.Formats.Exr.Compression; +using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; diff --git a/src/ImageSharp/Formats/Exr/ExrEncoder.cs b/src/ImageSharp/Formats/Exr/ExrEncoder.cs index 95913f2beb..15e10ba2fa 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoder.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoder.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Exr.Constants; + namespace SixLabors.ImageSharp.Formats.Exr; /// diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index 98f43f6e27..400915d220 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using System.Threading.Channels; using SixLabors.ImageSharp.Formats.Exr.Compression; +using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; diff --git a/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs b/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs index 29c30e7935..35611bb6eb 100644 --- a/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs +++ b/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Exr.Compression; +using SixLabors.ImageSharp.Formats.Exr.Constants; namespace SixLabors.ImageSharp.Formats.Exr; diff --git a/src/ImageSharp/Formats/Exr/ExrMetadata.cs b/src/ImageSharp/Formats/Exr/ExrMetadata.cs index 2e83826c96..552d2d1133 100644 --- a/src/ImageSharp/Formats/Exr/ExrMetadata.cs +++ b/src/ImageSharp/Formats/Exr/ExrMetadata.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Exr; diff --git a/src/ImageSharp/Formats/Exr/IExrEncoderOptions.cs b/src/ImageSharp/Formats/Exr/IExrEncoderOptions.cs index 0d90514e9c..2ccf26ec17 100644 --- a/src/ImageSharp/Formats/Exr/IExrEncoderOptions.cs +++ b/src/ImageSharp/Formats/Exr/IExrEncoderOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Formats.Exr.Constants; + namespace SixLabors.ImageSharp.Formats.Exr; /// From 137ebbba0654b91000cbfaa665467591c1f9df99 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 19 Mar 2026 19:02:47 +0100 Subject: [PATCH 35/66] Add Exr compressor factory --- .../Compression/Compressors/NoCompressor.cs | 8 ++++ .../Compression/Compressors/ZipCompressor.cs | 47 +++++++++++++++++++ .../Decompressors/B44Compression.cs | 4 +- .../Decompressors/NoneExrCompression.cs | 6 +-- .../Decompressors/RunLengthCompression.cs | 6 +-- .../Decompressors/ZipExrCompression.cs | 6 +-- .../Exr/Compression/ExrBaseCompression.cs | 23 +++++++-- .../Exr/Compression/ExrBaseDecompressor.cs | 4 +- .../Exr/Compression/ExrCompressorFactory.cs | 26 ++++++++++ .../Exr/Compression/ExrDecompressorFactory.cs | 23 ++++----- .../ExrCompression.cs} | 7 ++- .../Formats/Exr/ExrBaseCompressor.cs | 43 +++++++++++++++++ src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 26 +++++----- src/ImageSharp/Formats/Exr/ExrEncoder.cs | 5 ++ src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 5 +- .../Formats/Exr/ExrHeaderAttributes.cs | 5 +- src/ImageSharp/Formats/Exr/ExrThrowHelper.cs | 11 +++++ 17 files changed, 206 insertions(+), 49 deletions(-) create mode 100644 src/ImageSharp/Formats/Exr/Compression/Compressors/NoCompressor.cs create mode 100644 src/ImageSharp/Formats/Exr/Compression/Compressors/ZipCompressor.cs create mode 100644 src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs rename src/ImageSharp/Formats/Exr/{Compression/ExrCompressionType.cs => Constants/ExrCompression.cs} (91%) create mode 100644 src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoCompressor.cs new file mode 100644 index 0000000000..9a9bc8fff6 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoCompressor.cs @@ -0,0 +1,8 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors; + +internal class NoCompressor +{ +} diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipCompressor.cs new file mode 100644 index 0000000000..d205c2f1a3 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipCompressor.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors; + +internal class ZipCompressor : ExrBaseCompressor +{ + private readonly DeflateCompressionLevel compressionLevel; + + private readonly MemoryStream memoryStream = new(); + + public ZipCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, DeflateCompressionLevel compressionLevel) + : base(output, allocator, bytesPerBlock) + => this.compressionLevel = compressionLevel; + + /// + public override ExrCompression Method => ExrCompression.Zip; + + /// + public override void Initialize(int rowsPerStrip) + { + } + + /// + public override void CompressStrip(Span rows, int height) + { + this.memoryStream.Seek(0, SeekOrigin.Begin); + using (ZlibDeflateStream stream = new(this.Allocator, this.memoryStream, this.compressionLevel)) + { + stream.Write(rows); + stream.Flush(); + } + + int size = (int)this.memoryStream.Position; + byte[] buffer = this.memoryStream.GetBuffer(); + this.Output.Write(buffer, 0, size); + } + + /// + protected override void Dispose(bool disposing) + { + } +} diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44Compression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44Compression.cs index b8cb468142..d0f9eb6efe 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44Compression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44Compression.cs @@ -24,8 +24,8 @@ internal class B44Compression : ExrBaseDecompressor private IMemoryOwner tmpBuffer; - public B44Compression(MemoryAllocator allocator, uint uncompressedBytes, int width, int height, uint rowsPerBlock, int channelCount) - : base(allocator, uncompressedBytes) + public B44Compression(MemoryAllocator allocator, uint bytesPerBlock, int width, int height, uint rowsPerBlock, int channelCount) + : base(allocator, bytesPerBlock) { this.width = width; this.height = height; diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs index 6b36dffa2b..75bf67cb24 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs @@ -8,13 +8,13 @@ namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; internal class NoneExrCompression : ExrBaseDecompressor { - public NoneExrCompression(MemoryAllocator allocator, uint uncompressedBytes) - : base(allocator, uncompressedBytes) + public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock) + : base(allocator, bytesPerBlock) { } public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) - => stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.UncompressedBytes)); + => stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.BytesPerBlock)); protected override void Dispose(bool disposing) { diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthCompression.cs index f1c7bb759b..15538a813e 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthCompression.cs @@ -19,7 +19,7 @@ public RunLengthCompression(MemoryAllocator allocator, uint uncompressedBytes) public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) { Span uncompressed = this.tmpBuffer.GetSpan(); - int maxLength = (int)this.UncompressedBytes; + int maxLength = (int)this.BytesPerBlock; int offset = 0; while (compressedBytes > 0) { @@ -63,8 +63,8 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, } } - Reconstruct(uncompressed, this.UncompressedBytes); - Interleave(uncompressed, this.UncompressedBytes, buffer); + Reconstruct(uncompressed, this.BytesPerBlock); + Interleave(uncompressed, this.BytesPerBlock, buffer); } private static byte ReadNextByte(BufferedReadStream stream) diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs index 4d9b42732f..95d4c03137 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs @@ -13,8 +13,8 @@ internal class ZipExrCompression : ExrBaseDecompressor { private readonly IMemoryOwner tmpBuffer; - public ZipExrCompression(MemoryAllocator allocator, uint uncompressedBytes) - : base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate((int)uncompressedBytes); + public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock) + : base(allocator, bytesPerBlock) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) { @@ -28,7 +28,7 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, int left = (int)(compressedBytes - (stream.Position - pos)); return left > 0 ? left : 0; }); - inflateStream.AllocateNewBytes((int)this.UncompressedBytes, true); + inflateStream.AllocateNewBytes((int)this.BytesPerBlock, true); DeflateStream dataStream = inflateStream.CompressedStream!; int totalRead = 0; diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs index 3dab1006ed..e510135c2c 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs @@ -9,10 +9,10 @@ internal abstract class ExrBaseCompression : IDisposable { private bool isDisposed; - protected ExrBaseCompression(MemoryAllocator allocator, uint bytePerRow) + protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock) { this.Allocator = allocator; - this.UncompressedBytes = bytePerRow; + this.BytesPerBlock = bytesPerBlock; } /// @@ -21,9 +21,24 @@ protected ExrBaseCompression(MemoryAllocator allocator, uint bytePerRow) protected MemoryAllocator Allocator { get; } /// - /// Gets the uncompressed bytes. + /// Gets the bits per pixel. /// - public uint UncompressedBytes { get; } + public int BitsPerPixel { get; } + + /// + /// Gets the bytes per row. + /// + public int BytesPerRow { get; } + + /// + /// Gets the uncompressed bytes per block. + /// + public uint BytesPerBlock { get; } + + /// + /// Gets the image width. + /// + public int Width { get; } /// public void Dispose() diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs index 12edcc1046..0dd7d01108 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs @@ -8,8 +8,8 @@ namespace SixLabors.ImageSharp.Formats.Exr.Compression; internal abstract class ExrBaseDecompressor : ExrBaseCompression { - protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytePerRow) - : base(allocator, bytePerRow) + protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock) + : base(allocator, bytesPerBlock) { } diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs new file mode 100644 index 0000000000..fc403be3a0 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Exr.Compression.Compressors; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression; + +internal static class ExrCompressorFactory +{ + public static ExrBaseCompressor Create( + ExrCompression method, + Stream output, + MemoryAllocator allocator, + int width, + DeflateCompressionLevel compressionLevel) + { + switch (method) + { + default: + throw ExrThrowHelper.NotSupportedCompressor(method.ToString()); + } + } +} diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs index a293787fad..6764a2a130 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs @@ -2,26 +2,27 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; +using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Exr.Compression; internal static class ExrDecompressorFactory { - public static ExrBaseDecompressor Create(ExrCompressionType method, MemoryAllocator memoryAllocator, uint uncompressedBytes, int width, int height, uint rowsPerBlock, int channelCount) + public static ExrBaseDecompressor Create(ExrCompression method, MemoryAllocator memoryAllocator, uint bytesPerBlock, int width, int height, uint rowsPerBlock, int channelCount) { switch (method) { - case ExrCompressionType.None: - return new NoneExrCompression(memoryAllocator, uncompressedBytes); - case ExrCompressionType.Zips: - return new ZipExrCompression(memoryAllocator, uncompressedBytes); - case ExrCompressionType.Zip: - return new ZipExrCompression(memoryAllocator, uncompressedBytes); - case ExrCompressionType.RunLengthEncoded: - return new RunLengthCompression(memoryAllocator, uncompressedBytes); - case ExrCompressionType.B44: - return new B44Compression(memoryAllocator, uncompressedBytes, width, height, rowsPerBlock, channelCount); + case ExrCompression.None: + return new NoneExrCompression(memoryAllocator, bytesPerBlock); + case ExrCompression.Zips: + return new ZipExrCompression(memoryAllocator, bytesPerBlock); + case ExrCompression.Zip: + return new ZipExrCompression(memoryAllocator, bytesPerBlock); + case ExrCompression.RunLengthEncoded: + return new RunLengthCompression(memoryAllocator, bytesPerBlock); + case ExrCompression.B44: + return new B44Compression(memoryAllocator, bytesPerBlock, width, height, rowsPerBlock, channelCount); default: throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)); } diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrCompressionType.cs b/src/ImageSharp/Formats/Exr/Constants/ExrCompression.cs similarity index 91% rename from src/ImageSharp/Formats/Exr/Compression/ExrCompressionType.cs rename to src/ImageSharp/Formats/Exr/Constants/ExrCompression.cs index a7b584ed4c..d0964bf33b 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrCompressionType.cs +++ b/src/ImageSharp/Formats/Exr/Constants/ExrCompression.cs @@ -1,9 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -namespace SixLabors.ImageSharp.Formats.Exr.Compression; +namespace SixLabors.ImageSharp.Formats.Exr.Constants; -internal enum ExrCompressionType +/// +/// Enumeration representing the compression formats defined by the EXR file-format. +/// +public enum ExrCompression { /// /// Pixel data is not compressed. diff --git a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs new file mode 100644 index 0000000000..c2a752c614 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression; + +internal abstract class ExrBaseCompressor : ExrBaseCompression +{ + /// + /// Initializes a new instance of the class. + /// + /// The output stream to write the compressed image to. + /// The memory allocator. + /// Bytes per block. + protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock) + : base(allocator, bytesPerBlock) + => this.Output = output; + + /// + /// Gets the compression method to use. + /// + public abstract ExrCompression Method { get; } + + /// + /// Gets the output stream to write the compressed image to. + /// + public Stream Output { get; } + + /// + /// Does any initialization required for the compression. + /// + /// The number of rows per strip. + public abstract void Initialize(int rowsPerStrip); + + /// + /// Compresses a strip of the image. + /// + /// Image rows to compress. + /// Image height. + public abstract void CompressStrip(Span rows, int height); +} diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 38e9c3ec90..7f53d8a24b 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -79,7 +79,7 @@ public ExrDecoderCore(ExrDecoderOptions options) /// /// Gets or sets the compression method. /// - private ExrCompressionType Compression { get; set; } + private ExrCompression Compression { get; set; } /// /// Gets or sets the image data type, either RGB, RGBA or gray. @@ -453,7 +453,7 @@ private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) IList channels = null; ExrBox2i? dataWindow = null; - ExrCompressionType? compression = null; + ExrCompression? compression = null; ExrBox2i? displayWindow = null; ExrLineOrder? lineOrder = null; float? aspectRatio = null; @@ -471,7 +471,7 @@ private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) channels = this.ReadChannelList(stream, attribute.Length); break; case ExrConstants.AttributeNames.Compression: - compression = (ExrCompressionType)stream.ReadByte(); + compression = (ExrCompression)stream.ReadByte(); break; case ExrConstants.AttributeNames.DataWindow: dataWindow = this.ReadBoxInteger(stream); @@ -648,11 +648,11 @@ private bool IsSupportedCompression() { switch (this.Compression) { - case ExrCompressionType.None: - case ExrCompressionType.Zip: - case ExrCompressionType.Zips: - case ExrCompressionType.RunLengthEncoded: - case ExrCompressionType.B44: + case ExrCompression.None: + case ExrCompression.Zip: + case ExrCompression.Zips: + case ExrCompression.RunLengthEncoded: + case ExrCompression.B44: return true; } @@ -757,12 +757,12 @@ private uint RowsPerBlock() { switch (this.Compression) { - case ExrCompressionType.Zip: - case ExrCompressionType.Pxr24: + case ExrCompression.Zip: + case ExrCompression.Pxr24: return 16; - case ExrCompressionType.B44: - case ExrCompressionType.B44A: - case ExrCompressionType.Piz: + case ExrCompression.B44: + case ExrCompression.B44A: + case ExrCompression.Piz: return 32; default: diff --git a/src/ImageSharp/Formats/Exr/ExrEncoder.cs b/src/ImageSharp/Formats/Exr/ExrEncoder.cs index 15e10ba2fa..2ea1b91161 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoder.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoder.cs @@ -15,6 +15,11 @@ public sealed class ExrEncoder : ImageEncoder /// public ExrPixelType? PixelType { get; set; } + /// + /// Gets the compression type to use. + /// + public ExrCompression? Compression { get; init; } + /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index 400915d220..ff5287b752 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -6,7 +6,6 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Threading.Channels; -using SixLabors.ImageSharp.Formats.Exr.Compression; using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -77,7 +76,7 @@ public void Encode(Image image, Stream stream, CancellationToken this.pixelType ??= exrMetadata.PixelType; int width = image.Width; int height = image.Height; - ExrCompressionType compression = ExrCompressionType.None; + ExrCompression compression = ExrCompression.None; float aspectRatio = 1.0f; ExrBox2i dataWindow = new(0, 0, width - 1, height - 1); ExrBox2i displayWindow = new(0, 0, width - 1, height - 1); @@ -311,7 +310,7 @@ private void WriteChannels(Stream stream, IList channels) stream.WriteByte(0); } - private void WriteCompression(Stream stream, ExrCompressionType compression) + private void WriteCompression(Stream stream, ExrCompression compression) { this.WriteAttributeInformation(stream, ExrConstants.AttributeNames.Compression, ExrConstants.AttibuteTypes.Compression, 1); stream.WriteByte((byte)compression); diff --git a/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs b/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs index 35611bb6eb..afda62bf92 100644 --- a/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs +++ b/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Formats.Exr.Compression; using SixLabors.ImageSharp.Formats.Exr.Constants; namespace SixLabors.ImageSharp.Formats.Exr; @@ -10,7 +9,7 @@ internal class ExrHeaderAttributes { public ExrHeaderAttributes( IList channels, - ExrCompressionType compression, + ExrCompression compression, ExrBox2i dataWindow, ExrBox2i displayWindow, ExrLineOrder lineOrder, @@ -36,7 +35,7 @@ public ExrHeaderAttributes( public IList Channels { get; set; } - public ExrCompressionType Compression { get; set; } + public ExrCompression Compression { get; set; } public ExrBox2i DataWindow { get; set; } diff --git a/src/ImageSharp/Formats/Exr/ExrThrowHelper.cs b/src/ImageSharp/Formats/Exr/ExrThrowHelper.cs index f668dcfefd..51419ec95c 100644 --- a/src/ImageSharp/Formats/Exr/ExrThrowHelper.cs +++ b/src/ImageSharp/Formats/Exr/ExrThrowHelper.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; + namespace SixLabors.ImageSharp.Formats.Exr; /// @@ -8,15 +10,24 @@ namespace SixLabors.ImageSharp.Formats.Exr; /// internal static class ExrThrowHelper { + [DoesNotReturn] public static Exception NotSupportedDecompressor(string compressionType) => throw new NotSupportedException($"Not supported decoder compression method: {compressionType}"); + [DoesNotReturn] public static void ThrowInvalidImageContentException(string errorMessage) => throw new InvalidImageContentException(errorMessage); + [DoesNotReturn] public static void ThrowNotSupportedVersion() => throw new NotSupportedException("Unsupported EXR version"); + [DoesNotReturn] public static void ThrowNotSupported(string msg) => throw new NotSupportedException(msg); + [DoesNotReturn] public static void ThrowInvalidImageHeader() => throw new InvalidImageContentException("Invalid EXR image header"); + [DoesNotReturn] public static void ThrowInvalidImageHeader(string msg) => throw new InvalidImageContentException(msg); + + [DoesNotReturn] + public static Exception NotSupportedCompressor(string compressionType) => throw new NotSupportedException($"Not supported encoder compression method: {compressionType}"); } From 3ff9d244222c5802ff6d2c13b9278c1277cac0c3 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 21 Mar 2026 15:21:26 +0100 Subject: [PATCH 36/66] Refactor exr encoder for compression --- .../Compression/Compressors/NoCompressor.cs | 8 - .../Compressors/NoneExrCompressor.cs | 31 ++++ .../{ZipCompressor.cs => ZipExrCompressor.cs} | 6 +- ...B44Compression.cs => B44ExrCompression.cs} | 4 +- ...pression.cs => RunLengthExrCompression.cs} | 4 +- .../Exr/Compression/ExrCompressorFactory.cs | 11 +- .../Exr/Compression/ExrDecompressorFactory.cs | 13 +- .../Formats/Exr/ExrBaseCompressor.cs | 4 +- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 4 +- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 151 ++++++++++++++++-- 10 files changed, 196 insertions(+), 40 deletions(-) delete mode 100644 src/ImageSharp/Formats/Exr/Compression/Compressors/NoCompressor.cs create mode 100644 src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs rename src/ImageSharp/Formats/Exr/Compression/Compressors/{ZipCompressor.cs => ZipExrCompressor.cs} (83%) rename src/ImageSharp/Formats/Exr/Compression/Decompressors/{B44Compression.cs => B44ExrCompression.cs} (97%) rename src/ImageSharp/Formats/Exr/Compression/Decompressors/{RunLengthCompression.cs => RunLengthExrCompression.cs} (93%) diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoCompressor.cs deleted file mode 100644 index 9a9bc8fff6..0000000000 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoCompressor.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors; - -internal class NoCompressor -{ -} diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs new file mode 100644 index 0000000000..e8f3a0686f --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors; + +internal class NoneExrCompressor : ExrBaseCompressor +{ + public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock) + : base(output, allocator, bytesPerBlock) + { + } + + /// + public override ExrCompression Method => ExrCompression.Zip; + + /// + public override void Initialize(int rowsPerBlock) + { + } + + /// + public override void CompressStrip(Span rows, int height) => this.Output.Write(rows); + + /// + protected override void Dispose(bool disposing) + { + } +} diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs similarity index 83% rename from src/ImageSharp/Formats/Exr/Compression/Compressors/ZipCompressor.cs rename to src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs index d205c2f1a3..af93664f33 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -7,13 +7,13 @@ namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors; -internal class ZipCompressor : ExrBaseCompressor +internal class ZipExrCompressor : ExrBaseCompressor { private readonly DeflateCompressionLevel compressionLevel; private readonly MemoryStream memoryStream = new(); - public ZipCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, DeflateCompressionLevel compressionLevel) + public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, DeflateCompressionLevel compressionLevel) : base(output, allocator, bytesPerBlock) => this.compressionLevel = compressionLevel; @@ -21,7 +21,7 @@ public ZipCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBloc public override ExrCompression Method => ExrCompression.Zip; /// - public override void Initialize(int rowsPerStrip) + public override void Initialize(int rowsPerBlock) { } diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44Compression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs similarity index 97% rename from src/ImageSharp/Formats/Exr/Compression/Decompressors/B44Compression.cs rename to src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs index d0f9eb6efe..1c6afdfd91 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44Compression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; -internal class B44Compression : ExrBaseDecompressor +internal class B44ExrCompression : ExrBaseDecompressor { private readonly int width; @@ -24,7 +24,7 @@ internal class B44Compression : ExrBaseDecompressor private IMemoryOwner tmpBuffer; - public B44Compression(MemoryAllocator allocator, uint bytesPerBlock, int width, int height, uint rowsPerBlock, int channelCount) + public B44ExrCompression(MemoryAllocator allocator, uint bytesPerBlock, int width, int height, uint rowsPerBlock, int channelCount) : base(allocator, bytesPerBlock) { this.width = width; diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs similarity index 93% rename from src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthCompression.cs rename to src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs index 15538a813e..bc5a9a5579 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs @@ -7,13 +7,13 @@ namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; -internal class RunLengthCompression : ExrBaseDecompressor +internal class RunLengthExrCompression : ExrBaseDecompressor { private readonly IMemoryOwner tmpBuffer; private readonly ushort[] s = new ushort[16]; - public RunLengthCompression(MemoryAllocator allocator, uint uncompressedBytes) + public RunLengthExrCompression(MemoryAllocator allocator, uint uncompressedBytes) : base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate((int)uncompressedBytes); public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs index fc403be3a0..4482ca7633 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs @@ -12,13 +12,20 @@ internal static class ExrCompressorFactory { public static ExrBaseCompressor Create( ExrCompression method, - Stream output, MemoryAllocator allocator, + Stream output, int width, - DeflateCompressionLevel compressionLevel) + int height, + uint bytesPerBlock, + uint rowsPerBlock, + int channelCount, + DeflateCompressionLevel compressionLevel = DeflateCompressionLevel.DefaultCompression) { switch (method) { + case ExrCompression.None: + return new NoneExrCompressor(output, allocator, bytesPerBlock); + default: throw ExrThrowHelper.NotSupportedCompressor(method.ToString()); } diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs index 6764a2a130..fb620e9854 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs @@ -9,7 +9,14 @@ namespace SixLabors.ImageSharp.Formats.Exr.Compression; internal static class ExrDecompressorFactory { - public static ExrBaseDecompressor Create(ExrCompression method, MemoryAllocator memoryAllocator, uint bytesPerBlock, int width, int height, uint rowsPerBlock, int channelCount) + public static ExrBaseDecompressor Create( + ExrCompression method, + MemoryAllocator memoryAllocator, + int width, + int height, + uint bytesPerBlock, + uint rowsPerBlock, + int channelCount) { switch (method) { @@ -20,9 +27,9 @@ public static ExrBaseDecompressor Create(ExrCompression method, MemoryAllocator case ExrCompression.Zip: return new ZipExrCompression(memoryAllocator, bytesPerBlock); case ExrCompression.RunLengthEncoded: - return new RunLengthCompression(memoryAllocator, bytesPerBlock); + return new RunLengthExrCompression(memoryAllocator, bytesPerBlock); case ExrCompression.B44: - return new B44Compression(memoryAllocator, bytesPerBlock, width, height, rowsPerBlock, channelCount); + return new B44ExrCompression(memoryAllocator, bytesPerBlock, width, height, rowsPerBlock, channelCount); default: throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)); } diff --git a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs index c2a752c614..ee21acb703 100644 --- a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs +++ b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs @@ -31,8 +31,8 @@ protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytes /// /// Does any initialization required for the compression. /// - /// The number of rows per strip. - public abstract void Initialize(int rowsPerStrip); + /// The number of rows per block. + public abstract void Initialize(int rowsPerBlock); /// /// Compresses a strip of the image. diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 7f53d8a24b..c7c05a9c54 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -147,7 +147,7 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); - using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock, width, height, rowsPerBlock, channelCount); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, width, height, bytesPerBlock, rowsPerBlock, channelCount); for (uint y = 0; y < height; y += rowsPerBlock) { @@ -200,7 +200,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); - using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, bytesPerBlock, width, height, rowsPerBlock, channelCount); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, width, height, bytesPerBlock, rowsPerBlock, channelCount); for (uint y = 0; y < height; y += rowsPerBlock) { diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index ff5287b752..7151a642ab 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -5,7 +5,7 @@ using System.Buffers.Binary; using System.Numerics; using System.Runtime.CompilerServices; -using System.Threading.Channels; +using SixLabors.ImageSharp.Formats.Exr.Compression; using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -47,7 +47,7 @@ internal sealed class ExrEncoderCore /// Initializes a new instance of the class. /// /// The encoder with options. - /// The configuration. + /// The configuration. /// The memory manager. public ExrEncoderCore(ExrEncoder encoder, Configuration configuration, MemoryAllocator memoryAllocator) { @@ -126,7 +126,7 @@ public void Encode(Image image, Stream stream, CancellationToken { case ExrPixelType.Half: case ExrPixelType.Float: - this.EncodeFloatingPointPixelData(stream, pixels, width, height, rowSizeBytes); + this.EncodeFloatingPointPixelData(stream, pixels, width, height, rowSizeBytes, channels, compression); break; case ExrPixelType.UnsignedInt: this.EncodeUnsignedIntPixelData(stream, pixels, width, height, rowSizeBytes); @@ -134,43 +134,60 @@ public void Encode(Image image, Stream stream, CancellationToken } } - private void EncodeFloatingPointPixelData(Stream stream, Buffer2D pixels, int width, int height, uint rowSizeBytes) + private void EncodeFloatingPointPixelData( + Stream stream, + Buffer2D pixels, + int width, + int height, + uint rowSizeBytes, + List channels, + ExrCompression compression) where TPixel : unmanaged, IPixel { - using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3); + uint bytesPerRow = this.CalculateBytesPerRow(channels, (uint)width); + uint rowsPerBlock = this.RowsPerBlock(compression); + uint bytesPerBlock = bytesPerRow * rowsPerBlock; + int channelCount = channels.Count; + + using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3, AllocationOptions.Clean); + using IMemoryOwner blockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, width, height, bytesPerBlock, rowsPerBlock, channelCount); + for (int y = 0; y < height; y++) { + // Write row index. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); + stream.Write(this.buffer.AsSpan(0, 4)); + + // Write pixel row data size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, rowSizeBytes); + stream.Write(this.buffer.AsSpan(0, 4)); + Span pixelRowSpan = pixels.DangerousGetRowSpan(y); for (int x = 0; x < width; x++) { - var vector4 = pixelRowSpan[x].ToVector4(); + Vector4 vector4 = pixelRowSpan[x].ToVector4(); redBuffer[x] = vector4.X; greenBuffer[x] = vector4.Y; blueBuffer[x] = vector4.Z; } - // Write row index. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); - stream.Write(this.buffer.AsSpan(0, 4)); - - // Write pixel row data size. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, rowSizeBytes); - stream.Write(this.buffer.AsSpan(0, 4)); - switch (this.pixelType) { case ExrPixelType.Float: - this.WriteSingleRow(stream, width, blueBuffer, greenBuffer, redBuffer); + this.WriteSingleRow(blockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); break; case ExrPixelType.Half: - this.WriteHalfSingleRow(stream, width, blueBuffer, greenBuffer, redBuffer); + this.WriteHalfSingleRow(blockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); break; } + + compressor.CompressStrip(blockBuffer.GetSpan(), 1); } } @@ -240,6 +257,50 @@ private void WriteSingleRow(Stream stream, int width, Span blueBuffer, Sp } } + private void WriteSingleRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + this.WriteSingle(buffer.Slice(offset), blueBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + this.WriteSingle(buffer.Slice(offset), greenBuffer[x]); + offset += 4; + } + + for (int x = 0; x < width; x++) + { + this.WriteSingle(buffer.Slice(offset), redBuffer[x]); + offset += 4; + } + } + + private void WriteHalfSingleRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + { + int offset = 0; + for (int x = 0; x < width; x++) + { + this.WriteHalfSingle(buffer.Slice(offset), blueBuffer[x]); + offset += 2; + } + + for (int x = 0; x < width; x++) + { + this.WriteHalfSingle(buffer.Slice(offset), greenBuffer[x]); + offset += 2; + } + + for (int x = 0; x < width; x++) + { + this.WriteHalfSingle(buffer.Slice(offset), redBuffer[x]); + offset += 2; + } + } + private void WriteHalfSingleRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) { for (int x = 0; x < width; x++) @@ -420,6 +481,12 @@ private unsafe void WriteSingle(Stream stream, float value) stream.Write(this.buffer.AsSpan(0, 4)); } + [MethodImpl(InliningOptions.ShortMethod)] + private unsafe void WriteSingle(Span buffer, float value) + { + BinaryPrimitives.WriteInt32LittleEndian(buffer, *(int*)&value); + } + [MethodImpl(InliningOptions.ShortMethod)] private void WriteHalfSingle(Stream stream, float value) { @@ -428,10 +495,62 @@ private void WriteHalfSingle(Stream stream, float value) stream.Write(this.buffer.AsSpan(0, 2)); } + + [MethodImpl(InliningOptions.ShortMethod)] + private void WriteHalfSingle(Span buffer, float value) + { + ushort valueAsShort = HalfTypeHelper.Pack(value); + BinaryPrimitives.WriteUInt16LittleEndian(this.buffer, valueAsShort); + } + [MethodImpl(InliningOptions.ShortMethod)] private void WriteUnsignedInt(Stream stream, uint value) { BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, value); stream.Write(this.buffer.AsSpan(0, 4)); } + + // TODO: avoid code duplication: This code is duplicate in the decoder. + private uint CalculateBytesPerRow(List channels, uint width) + { + uint bytesPerRow = 0; + foreach (ExrChannelInfo channelInfo in channels) + { + if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("R", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("G", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("B", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal)) + { + if (channelInfo.PixelType == ExrPixelType.Half) + { + bytesPerRow += 2 * width; + } + else + { + bytesPerRow += 4 * width; + } + } + } + + return bytesPerRow; + } + + // TODO: avoid code duplication: This code is duplicate in the decoder. + private uint RowsPerBlock(ExrCompression compression) + { + switch (compression) + { + case ExrCompression.Zip: + case ExrCompression.Pxr24: + return 16; + case ExrCompression.B44: + case ExrCompression.B44A: + case ExrCompression.Piz: + return 32; + + default: + return 1; + } + } } From 0d515147d019f6539f453b10f9bfbedcc4331195 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 21 Mar 2026 19:57:10 +0100 Subject: [PATCH 37/66] Use compressor when writing pixel row data with exr encoder --- .../Compressors/NoneExrCompressor.cs | 6 +- .../Compressors/ZipExrCompressor.cs | 3 +- .../Exr/Compression/ExrCompressorFactory.cs | 2 + .../Formats/Exr/ExrBaseCompressor.cs | 5 +- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 2 +- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 179 ++++++++++-------- 6 files changed, 110 insertions(+), 87 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs index e8f3a0686f..40ee81cf03 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs @@ -22,7 +22,11 @@ public override void Initialize(int rowsPerBlock) } /// - public override void CompressStrip(Span rows, int height) => this.Output.Write(rows); + public override uint CompressRowBlock(Span rows, int height) + { + this.Output.Write(rows); + return (uint)rows.Length; + } /// protected override void Dispose(bool disposing) diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs index af93664f33..e7b695df0c 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -26,7 +26,7 @@ public override void Initialize(int rowsPerBlock) } /// - public override void CompressStrip(Span rows, int height) + public override uint CompressRowBlock(Span rows, int height) { this.memoryStream.Seek(0, SeekOrigin.Begin); using (ZlibDeflateStream stream = new(this.Allocator, this.memoryStream, this.compressionLevel)) @@ -38,6 +38,7 @@ public override void CompressStrip(Span rows, int height) int size = (int)this.memoryStream.Position; byte[] buffer = this.memoryStream.GetBuffer(); this.Output.Write(buffer, 0, size); + return (uint)buffer.Length; } /// diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs index 4482ca7633..e8e7af4712 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs @@ -25,6 +25,8 @@ public static ExrBaseCompressor Create( { case ExrCompression.None: return new NoneExrCompressor(output, allocator, bytesPerBlock); + case ExrCompression.Zip: + return new ZipExrCompressor(output, allocator, bytesPerBlock, compressionLevel); default: throw ExrThrowHelper.NotSupportedCompressor(method.ToString()); diff --git a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs index ee21acb703..cf547eddb9 100644 --- a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs +++ b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs @@ -35,9 +35,10 @@ protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytes public abstract void Initialize(int rowsPerBlock); /// - /// Compresses a strip of the image. + /// Compresses a block of rows of the image. /// /// Image rows to compress. /// Image height. - public abstract void CompressStrip(Span rows, int height); + /// Number of bytes of of the compressed data. + public abstract uint CompressRowBlock(Span rows, int height); } diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index c7c05a9c54..7165720d30 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -53,7 +53,7 @@ public ExrDecoderCore(ExrDecoderOptions options) : base(options.GeneralOptions) { this.configuration = options.GeneralOptions.Configuration; - this.memoryAllocator = this.configuration.MemoryAllocator; + this.memoryAllocator = this.configuration.MemoryAllocator; } /// diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index 7151a642ab..9e63413445 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -5,6 +5,7 @@ using System.Buffers.Binary; using System.Numerics; using System.Runtime.CompilerServices; +using System.Threading.Channels; using SixLabors.ImageSharp.Formats.Exr.Compression; using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.Memory; @@ -54,8 +55,14 @@ public ExrEncoderCore(ExrEncoder encoder, Configuration configuration, MemoryAll this.configuration = configuration; this.encoder = encoder; this.memoryAllocator = memoryAllocator; + this.Compression = encoder.Compression ?? ExrCompression.None; } + /// + /// Gets or sets the compression implementation to use when encoding the image. + /// + internal ExrCompression Compression { get; set; } + /// /// Encodes the image to the specified stream from the . /// @@ -76,7 +83,6 @@ public void Encode(Image image, Stream stream, CancellationToken this.pixelType ??= exrMetadata.PixelType; int width = image.Width; int height = image.Height; - ExrCompression compression = ExrCompression.None; float aspectRatio = 1.0f; ExrBox2i dataWindow = new(0, 0, width - 1, height - 1); ExrBox2i displayWindow = new(0, 0, width - 1, height - 1); @@ -91,7 +97,7 @@ public void Encode(Image image, Stream stream, CancellationToken ]; ExrHeaderAttributes header = new( channels, - compression, + this.Compression, dataWindow, displayWindow, lineOrder, @@ -115,31 +121,40 @@ public void Encode(Image image, Stream stream, CancellationToken // Write EXR header. this.WriteHeader(stream, header); - // Write offsets to each pixel row. + // Next is offsets table to each pixel row, which will be written after the pixel data was written. int bytesPerChannel = this.pixelType == ExrPixelType.Half ? 2 : 4; int numberOfChannels = 3; uint rowSizeBytes = (uint)(width * numberOfChannels * bytesPerChannel); - this.WriteRowOffsets(stream, height, rowSizeBytes); + ulong startOfRowOffsetData = (ulong)stream.Position; + stream.Position += 8 * height; // Write pixel data. switch (this.pixelType) { case ExrPixelType.Half: case ExrPixelType.Float: - this.EncodeFloatingPointPixelData(stream, pixels, width, height, rowSizeBytes, channels, compression); + { + ulong[] rowOffsets = this.EncodeFloatingPointPixelData(stream, pixels, width, height, channels, this.Compression); + stream.Position = (long)startOfRowOffsetData; + this.WriteRowOffsets(stream, height, rowOffsets); break; + } + case ExrPixelType.UnsignedInt: - this.EncodeUnsignedIntPixelData(stream, pixels, width, height, rowSizeBytes); + { + ulong[] rowOffsets = this.EncodeUnsignedIntPixelData(stream, pixels, width, height, channels, this.Compression); + stream.Position = (long)startOfRowOffsetData; + this.WriteRowOffsets(stream, height, rowOffsets); break; + } } } - private void EncodeFloatingPointPixelData( + private ulong[] EncodeFloatingPointPixelData( Stream stream, Buffer2D pixels, int width, int height, - uint rowSizeBytes, List channels, ExrCompression compression) where TPixel : unmanaged, IPixel @@ -150,24 +165,26 @@ private void EncodeFloatingPointPixelData( int channelCount = channels.Count; using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3, AllocationOptions.Clean); - using IMemoryOwner blockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); + using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, width, height, bytesPerBlock, rowsPerBlock, channelCount); + ulong[] rowOffsets = new ulong[height]; for (int y = 0; y < height; y++) { + rowOffsets[y] = (ulong)stream.Position; + // Write row index. BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); stream.Write(this.buffer.AsSpan(0, 4)); - // Write pixel row data size. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, rowSizeBytes); - stream.Write(this.buffer.AsSpan(0, 4)); - + // At this point, it is not yet known how mcuh bytes the compressed data will take up, keep stream position. + long pixelDataSizePos = stream.Position; Span pixelRowSpan = pixels.DangerousGetRowSpan(y); + stream.Position = pixelDataSizePos + 4; for (int x = 0; x < width; x++) { @@ -177,32 +194,67 @@ private void EncodeFloatingPointPixelData( blueBuffer[x] = vector4.Z; } + // Write pixel data to buffer. switch (this.pixelType) { case ExrPixelType.Float: - this.WriteSingleRow(blockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); + this.WriteSingleRow(rowBlockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); break; case ExrPixelType.Half: - this.WriteHalfSingleRow(blockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); + this.WriteHalfSingleRow(rowBlockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); break; } - compressor.CompressStrip(blockBuffer.GetSpan(), 1); + // Write compressed pixel row data to stream. + uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), 1); + long positionAfterPixelData = stream.Position; + + // Write pixel row data size. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes); + stream.Position = pixelDataSizePos; + stream.Write(this.buffer.AsSpan(0, 4)); + stream.Position = positionAfterPixelData; } + + return rowOffsets; } - private void EncodeUnsignedIntPixelData(Stream stream, Buffer2D pixels, int width, int height, uint rowSizeBytes) + private ulong[] EncodeUnsignedIntPixelData( + Stream stream, + Buffer2D pixels, + int width, + int height, + List channels, + ExrCompression compression) where TPixel : unmanaged, IPixel { - using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3); + uint bytesPerRow = this.CalculateBytesPerRow(channels, (uint)width); + uint rowsPerBlock = this.RowsPerBlock(compression); + uint bytesPerBlock = bytesPerRow * rowsPerBlock; + int channelCount = channels.Count; + + using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3, AllocationOptions.Clean); + using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, width, height, bytesPerBlock, rowsPerBlock, channelCount); + Rgb96 rgb = default; + ulong[] rowOffsets = new ulong[height]; for (int y = 0; y < height; y++) { + rowOffsets[y] = (ulong)stream.Position; + + // Write row index. + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); + stream.Write(this.buffer.AsSpan(0, 4)); + + // At this point, it is not yet known how mcuh bytes the compressed data will take up, keep stream position. + long pixelDataSizePos = stream.Position; Span pixelRowSpan = pixels.DangerousGetRowSpan(y); + stream.Position = pixelDataSizePos + 4; for (int x = 0; x < width; x++) { @@ -214,16 +266,21 @@ private void EncodeUnsignedIntPixelData(Stream stream, Buffer2D blueBuffer[x] = rgb.B; } - // Write row index. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); - stream.Write(this.buffer.AsSpan(0, 4)); + // Write row data to buffer. + this.WriteUnsignedIntRow(rowBlockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); + + // Write pixel row data compressed to the stream. + uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), 1); + long positionAfterPixelData = stream.Position; // Write pixel row data size. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, rowSizeBytes); + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, compressedBytes); + stream.Position = pixelDataSizePos; stream.Write(this.buffer.AsSpan(0, 4)); - - this.WriteUnsignedIntRow(stream, width, blueBuffer, greenBuffer, redBuffer); + stream.Position = positionAfterPixelData; } + + return rowOffsets; } private void WriteHeader(Stream stream, ExrHeaderAttributes header) @@ -239,24 +296,6 @@ private void WriteHeader(Stream stream, ExrHeaderAttributes header) stream.WriteByte(0); } - private void WriteSingleRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) - { - for (int x = 0; x < width; x++) - { - this.WriteSingle(stream, blueBuffer[x]); - } - - for (int x = 0; x < width; x++) - { - this.WriteSingle(stream, greenBuffer[x]); - } - - for (int x = 0; x < width; x++) - { - this.WriteSingle(stream, redBuffer[x]); - } - } - private void WriteSingleRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) { int offset = 0; @@ -301,51 +340,36 @@ private void WriteHalfSingleRow(Span buffer, int width, Span blueBu } } - private void WriteHalfSingleRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) - { - for (int x = 0; x < width; x++) - { - this.WriteHalfSingle(stream, blueBuffer[x]); - } - - for (int x = 0; x < width; x++) - { - this.WriteHalfSingle(stream, greenBuffer[x]); - } - - for (int x = 0; x < width; x++) - { - this.WriteHalfSingle(stream, redBuffer[x]); - } - } - - private void WriteUnsignedIntRow(Stream stream, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + private void WriteUnsignedIntRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) { + int offset = 0; for (int x = 0; x < width; x++) { - this.WriteUnsignedInt(stream, blueBuffer[x]); + this.WriteUnsignedInt(buffer.Slice(offset), blueBuffer[x]); + offset += 4; } for (int x = 0; x < width; x++) { - this.WriteUnsignedInt(stream, greenBuffer[x]); + this.WriteUnsignedInt(buffer.Slice(offset), greenBuffer[x]); + offset += 4; } for (int x = 0; x < width; x++) { - this.WriteUnsignedInt(stream, redBuffer[x]); + this.WriteUnsignedInt(buffer.Slice(offset), redBuffer[x]); + offset += 4; } } - private void WriteRowOffsets(Stream stream, int height, uint rowSizeBytes) + private void WriteRowOffsets(Stream stream, int height, ulong[] rowOffsets) { - ulong startOfPixelData = (ulong)stream.Position + (8 * (ulong)height); - ulong offset = startOfPixelData; + ulong startOfRowOffsetData = (ulong)stream.Position; + ulong offset = startOfRowOffsetData; for (int i = 0; i < height; i++) { - BinaryPrimitives.WriteUInt64LittleEndian(this.buffer, offset); + BinaryPrimitives.WriteUInt64LittleEndian(this.buffer, rowOffsets[i]); stream.Write(this.buffer); - offset += 4 + 4 + rowSizeBytes; } } @@ -482,19 +506,7 @@ private unsafe void WriteSingle(Stream stream, float value) } [MethodImpl(InliningOptions.ShortMethod)] - private unsafe void WriteSingle(Span buffer, float value) - { - BinaryPrimitives.WriteInt32LittleEndian(buffer, *(int*)&value); - } - - [MethodImpl(InliningOptions.ShortMethod)] - private void WriteHalfSingle(Stream stream, float value) - { - ushort valueAsShort = HalfTypeHelper.Pack(value); - BinaryPrimitives.WriteUInt16LittleEndian(this.buffer, valueAsShort); - stream.Write(this.buffer.AsSpan(0, 2)); - } - + private unsafe void WriteSingle(Span buffer, float value) => BinaryPrimitives.WriteInt32LittleEndian(buffer, *(int*)&value); [MethodImpl(InliningOptions.ShortMethod)] private void WriteHalfSingle(Span buffer, float value) @@ -510,6 +522,9 @@ private void WriteUnsignedInt(Stream stream, uint value) stream.Write(this.buffer.AsSpan(0, 4)); } + [MethodImpl(InliningOptions.ShortMethod)] + private void WriteUnsignedInt(Span buffer, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + // TODO: avoid code duplication: This code is duplicate in the decoder. private uint CalculateBytesPerRow(List channels, uint width) { From 614a7f6e84bfe13b5224913490ffabd2601d1f1a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 22 Mar 2026 16:22:55 +0100 Subject: [PATCH 38/66] Re-arrange pixeldata and add predictor for zip compressed data --- .../Compressors/ZipExrCompressor.cs | 25 +++++++++++++++++-- .../Exr/Compression/ExrCompressorFactory.cs | 2 +- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs index e7b695df0c..8373e92e2e 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -11,7 +11,7 @@ internal class ZipExrCompressor : ExrBaseCompressor { private readonly DeflateCompressionLevel compressionLevel; - private readonly MemoryStream memoryStream = new(); + private MemoryStream memoryStream = new(); public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, DeflateCompressionLevel compressionLevel) : base(output, allocator, bytesPerBlock) @@ -28,6 +28,25 @@ public override void Initialize(int rowsPerBlock) /// public override uint CompressRowBlock(Span rows, int height) { + // Re-oder pixel values. + int n = rows.Length; + int t1 = 0; + int t2 = (n + 1) >> 1; + for (int i = 0; i < n; i++) + { + bool isOdd = (i & 1) == 1; + rows[isOdd ? t2++ : t1++] = rows[i]; + } + + // Predictor. + byte p = rows[0]; + for (int i = 1; i < rows.Length; i++) + { + int d = (rows[i] - p + 128 + 256) & 255; + p = rows[i]; + rows[i] = (byte)d; + } + this.memoryStream.Seek(0, SeekOrigin.Begin); using (ZlibDeflateStream stream = new(this.Allocator, this.memoryStream, this.compressionLevel)) { @@ -38,7 +57,9 @@ public override uint CompressRowBlock(Span rows, int height) int size = (int)this.memoryStream.Position; byte[] buffer = this.memoryStream.GetBuffer(); this.Output.Write(buffer, 0, size); - return (uint)buffer.Length; + + this.memoryStream = new(); + return (uint)size; } /// diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs index e8e7af4712..a87372748a 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs @@ -25,7 +25,7 @@ public static ExrBaseCompressor Create( { case ExrCompression.None: return new NoneExrCompressor(output, allocator, bytesPerBlock); - case ExrCompression.Zip: + case ExrCompression.Zips: return new ZipExrCompressor(output, allocator, bytesPerBlock, compressionLevel); default: From 5ee52ef403cfcd4a9b70e014a3fa2ded1eecabf1 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 22 Mar 2026 19:26:58 +0100 Subject: [PATCH 39/66] Fix issue in re-ordering pixel value, does not work inplace --- .../Compressors/ZipExrCompressor.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs index 8373e92e2e..17203b6ba3 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -13,9 +13,14 @@ internal class ZipExrCompressor : ExrBaseCompressor private MemoryStream memoryStream = new(); + private readonly System.Buffers.IMemoryOwner buffer; + public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, DeflateCompressionLevel compressionLevel) : base(output, allocator, bytesPerBlock) - => this.compressionLevel = compressionLevel; + { + this.compressionLevel = compressionLevel; + this.buffer = allocator.Allocate((int)bytesPerBlock); + } /// public override ExrCompression Method => ExrCompression.Zip; @@ -30,27 +35,29 @@ public override uint CompressRowBlock(Span rows, int height) { // Re-oder pixel values. int n = rows.Length; + Span reordered = this.buffer.GetSpan(); int t1 = 0; int t2 = (n + 1) >> 1; for (int i = 0; i < n; i++) { bool isOdd = (i & 1) == 1; - rows[isOdd ? t2++ : t1++] = rows[i]; + reordered[isOdd ? t2++ : t1++] = rows[i]; } // Predictor. - byte p = rows[0]; + Span predicted = reordered; + byte p = predicted[0]; for (int i = 1; i < rows.Length; i++) { - int d = (rows[i] - p + 128 + 256) & 255; - p = rows[i]; - rows[i] = (byte)d; + int d = (predicted[i] - p + 128 + 256) & 255; + p = predicted[i]; + predicted[i] = (byte)d; } this.memoryStream.Seek(0, SeekOrigin.Begin); using (ZlibDeflateStream stream = new(this.Allocator, this.memoryStream, this.compressionLevel)) { - stream.Write(rows); + stream.Write(predicted); stream.Flush(); } From f212af812db899779395d6a105b5e69a86054e28 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 23 Mar 2026 16:04:38 +0100 Subject: [PATCH 40/66] Cleanup Compressor/Decompressors: Remove not needed fields --- .../Compressors/NoneExrCompressor.cs | 11 +++------- .../Compressors/ZipExrCompressor.cs | 11 +++------- .../Decompressors/B44ExrCompression.cs | 13 ++++++------ .../Decompressors/NoneExrCompression.cs | 6 ++++-- .../Decompressors/RunLengthExrCompression.cs | 6 ++++-- .../Decompressors/ZipExrCompression.cs | 6 ++++-- .../Exr/Compression/ExrBaseCompression.cs | 5 +++-- .../Exr/Compression/ExrBaseDecompressor.cs | 4 ++-- .../Exr/Compression/ExrCompressorFactory.cs | 11 +++++----- .../Exr/Compression/ExrDecompressorFactory.cs | 12 +++++------ .../Formats/Exr/ExrBaseCompressor.cs | 17 ++++++---------- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 4 ++-- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 20 +++++++++---------- 13 files changed, 58 insertions(+), 68 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs index 40ee81cf03..cd9bec6131 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs @@ -8,8 +8,8 @@ namespace SixLabors.ImageSharp.Formats.Exr.Compression.Compressors; internal class NoneExrCompressor : ExrBaseCompressor { - public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock) - : base(output, allocator, bytesPerBlock) + public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) + : base(output, allocator, bytesPerBlock, bytesPerRow) { } @@ -17,12 +17,7 @@ public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPer public override ExrCompression Method => ExrCompression.Zip; /// - public override void Initialize(int rowsPerBlock) - { - } - - /// - public override uint CompressRowBlock(Span rows, int height) + public override uint CompressRowBlock(Span rows, int rowCount) { this.Output.Write(rows); return (uint)rows.Length; diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs index 17203b6ba3..6b82b70777 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -15,8 +15,8 @@ internal class ZipExrCompressor : ExrBaseCompressor private readonly System.Buffers.IMemoryOwner buffer; - public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, DeflateCompressionLevel compressionLevel) - : base(output, allocator, bytesPerBlock) + public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, DeflateCompressionLevel compressionLevel) + : base(output, allocator, bytesPerBlock, bytesPerRow) { this.compressionLevel = compressionLevel; this.buffer = allocator.Allocate((int)bytesPerBlock); @@ -26,12 +26,7 @@ public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerB public override ExrCompression Method => ExrCompression.Zip; /// - public override void Initialize(int rowsPerBlock) - { - } - - /// - public override uint CompressRowBlock(Span rows, int height) + public override uint CompressRowBlock(Span rows, int rowCount) { // Re-oder pixel values. int n = rows.Length; diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs index 1c6afdfd91..bf6d9929e2 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs @@ -12,28 +12,26 @@ internal class B44ExrCompression : ExrBaseDecompressor { private readonly int width; - private readonly int height; - private readonly uint rowsPerBlock; private readonly int channelCount; - private byte[] scratch = new byte[14]; + private readonly byte[] scratch = new byte[14]; private ushort[] s = new ushort[16]; - private IMemoryOwner tmpBuffer; + private readonly IMemoryOwner tmpBuffer; - public B44ExrCompression(MemoryAllocator allocator, uint bytesPerBlock, int width, int height, uint rowsPerBlock, int channelCount) - : base(allocator, bytesPerBlock) + public B44ExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount) + : base(allocator, bytesPerBlock, bytesPerRow) { this.width = width; - this.height = height; this.rowsPerBlock = rowsPerBlock; this.channelCount = channelCount; this.tmpBuffer = allocator.Allocate((int)(width * rowsPerBlock * channelCount)); } + /// public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) { Span outputBuffer = MemoryMarshal.Cast(buffer); @@ -187,5 +185,6 @@ private static void Unpack3(Span b, Span s) } } + /// protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); } diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs index 75bf67cb24..26c69db8c6 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs @@ -8,14 +8,16 @@ namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; internal class NoneExrCompression : ExrBaseDecompressor { - public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock) - : base(allocator, bytesPerBlock) + public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) + : base(allocator, bytesPerBlock, bytesPerRow) { } + /// public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) => stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.BytesPerBlock)); + /// protected override void Dispose(bool disposing) { } diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs index bc5a9a5579..12f5fc8ab6 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs @@ -13,9 +13,10 @@ internal class RunLengthExrCompression : ExrBaseDecompressor private readonly ushort[] s = new ushort[16]; - public RunLengthExrCompression(MemoryAllocator allocator, uint uncompressedBytes) - : base(allocator, uncompressedBytes) => this.tmpBuffer = allocator.Allocate((int)uncompressedBytes); + public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) + : base(allocator, bytesPerBlock, bytesPerRow) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + /// public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) { Span uncompressed = this.tmpBuffer.GetSpan(); @@ -78,5 +79,6 @@ private static byte ReadNextByte(BufferedReadStream stream) return (byte)nextByte; } + /// protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); } diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs index 95d4c03137..5424634523 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs @@ -13,9 +13,10 @@ internal class ZipExrCompression : ExrBaseDecompressor { private readonly IMemoryOwner tmpBuffer; - public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock) - : base(allocator, bytesPerBlock) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) + : base(allocator, bytesPerBlock, bytesPerRow) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + /// public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) { Span uncompressed = this.tmpBuffer.GetSpan(); @@ -52,5 +53,6 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, Interleave(uncompressed, (uint)totalRead, buffer); } + /// protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); } diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs index e510135c2c..57e6b2a26c 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs @@ -9,10 +9,11 @@ internal abstract class ExrBaseCompression : IDisposable { private bool isDisposed; - protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock) + protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) { this.Allocator = allocator; this.BytesPerBlock = bytesPerBlock; + this.BytesPerRow = bytesPerRow; } /// @@ -28,7 +29,7 @@ protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock) /// /// Gets the bytes per row. /// - public int BytesPerRow { get; } + public uint BytesPerRow { get; } /// /// Gets the uncompressed bytes per block. diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs index 0dd7d01108..1bbf36d768 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs @@ -8,8 +8,8 @@ namespace SixLabors.ImageSharp.Formats.Exr.Compression; internal abstract class ExrBaseDecompressor : ExrBaseCompression { - protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock) - : base(allocator, bytesPerBlock) + protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) + : base(allocator, bytesPerBlock, bytesPerRow) { } diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs index a87372748a..24f396e16f 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs @@ -14,19 +14,18 @@ public static ExrBaseCompressor Create( ExrCompression method, MemoryAllocator allocator, Stream output, - int width, - int height, uint bytesPerBlock, - uint rowsPerBlock, - int channelCount, + uint bytesPerRow, DeflateCompressionLevel compressionLevel = DeflateCompressionLevel.DefaultCompression) { switch (method) { case ExrCompression.None: - return new NoneExrCompressor(output, allocator, bytesPerBlock); + return new NoneExrCompressor(output, allocator, bytesPerBlock, bytesPerRow); case ExrCompression.Zips: - return new ZipExrCompressor(output, allocator, bytesPerBlock, compressionLevel); + return new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, compressionLevel); + case ExrCompression.Zip: + return new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, compressionLevel); default: throw ExrThrowHelper.NotSupportedCompressor(method.ToString()); diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs index fb620e9854..2696a289cd 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs @@ -13,23 +13,23 @@ public static ExrBaseDecompressor Create( ExrCompression method, MemoryAllocator memoryAllocator, int width, - int height, uint bytesPerBlock, + uint bytesPerRow, uint rowsPerBlock, int channelCount) { switch (method) { case ExrCompression.None: - return new NoneExrCompression(memoryAllocator, bytesPerBlock); + return new NoneExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow); case ExrCompression.Zips: - return new ZipExrCompression(memoryAllocator, bytesPerBlock); + return new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow); case ExrCompression.Zip: - return new ZipExrCompression(memoryAllocator, bytesPerBlock); + return new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow); case ExrCompression.RunLengthEncoded: - return new RunLengthExrCompression(memoryAllocator, bytesPerBlock); + return new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow); case ExrCompression.B44: - return new B44ExrCompression(memoryAllocator, bytesPerBlock, width, height, rowsPerBlock, channelCount); + return new B44ExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount); default: throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)); } diff --git a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs index cf547eddb9..1f464ec675 100644 --- a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs +++ b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs @@ -13,9 +13,10 @@ internal abstract class ExrBaseCompressor : ExrBaseCompression /// /// The output stream to write the compressed image to. /// The memory allocator. - /// Bytes per block. - protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock) - : base(allocator, bytesPerBlock) + /// Bytes per row block. + /// Bytes per pixel row. + protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) + : base(allocator, bytesPerBlock, bytesPerRow) => this.Output = output; /// @@ -28,17 +29,11 @@ protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytes /// public Stream Output { get; } - /// - /// Does any initialization required for the compression. - /// - /// The number of rows per block. - public abstract void Initialize(int rowsPerBlock); - /// /// Compresses a block of rows of the image. /// /// Image rows to compress. - /// Image height. + /// The number of rows to compress. /// Number of bytes of of the compressed data. - public abstract uint CompressRowBlock(Span rows, int height); + public abstract uint CompressRowBlock(Span rows, int rowCount); } diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 7165720d30..dbc6b7bf71 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -147,7 +147,7 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); - using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, width, height, bytesPerBlock, rowsPerBlock, channelCount); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, width, bytesPerBlock, bytesPerRow, rowsPerBlock, channelCount); for (uint y = 0; y < height; y += rowsPerBlock) { @@ -200,7 +200,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); - using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, width, height, bytesPerBlock, rowsPerBlock, channelCount); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, width, bytesPerBlock, bytesPerRow, rowsPerBlock, channelCount); for (uint y = 0; y < height; y += rowsPerBlock) { diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index 9e63413445..a7b37eda28 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -5,7 +5,6 @@ using System.Buffers.Binary; using System.Numerics; using System.Runtime.CompilerServices; -using System.Threading.Channels; using SixLabors.ImageSharp.Formats.Exr.Compression; using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.Memory; @@ -32,7 +31,7 @@ internal sealed class ExrEncoderCore /// /// The global configuration. /// - private Configuration configuration; + private readonly Configuration configuration; /// /// The encoder with options. @@ -170,7 +169,7 @@ private ulong[] EncodeFloatingPointPixelData( Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); - using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, width, height, bytesPerBlock, rowsPerBlock, channelCount); + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow); ulong[] rowOffsets = new ulong[height]; for (int y = 0; y < height; y++) @@ -181,7 +180,7 @@ private ulong[] EncodeFloatingPointPixelData( BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); stream.Write(this.buffer.AsSpan(0, 4)); - // At this point, it is not yet known how mcuh bytes the compressed data will take up, keep stream position. + // At this point, it is not yet known how much bytes the compressed data will take up, keep stream position. long pixelDataSizePos = stream.Position; Span pixelRowSpan = pixels.DangerousGetRowSpan(y); stream.Position = pixelDataSizePos + 4; @@ -195,18 +194,19 @@ private ulong[] EncodeFloatingPointPixelData( } // Write pixel data to buffer. + Span rowBlockSpan = rowBlockBuffer.GetSpan(); switch (this.pixelType) { case ExrPixelType.Float: - this.WriteSingleRow(rowBlockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); + this.WriteSingleRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer); break; case ExrPixelType.Half: - this.WriteHalfSingleRow(rowBlockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); + this.WriteHalfSingleRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer); break; } - // Write compressed pixel row data to stream. - uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), 1); + // Write compressed pixel row data to the stream. + uint compressedBytes = compressor.CompressRowBlock(rowBlockSpan, (int)rowsPerBlock); long positionAfterPixelData = stream.Position; // Write pixel row data size. @@ -239,7 +239,7 @@ private ulong[] EncodeUnsignedIntPixelData( Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); - using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, width, height, bytesPerBlock, rowsPerBlock, channelCount); + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow); Rgb96 rgb = default; ulong[] rowOffsets = new ulong[height]; @@ -251,7 +251,7 @@ private ulong[] EncodeUnsignedIntPixelData( BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); stream.Write(this.buffer.AsSpan(0, 4)); - // At this point, it is not yet known how mcuh bytes the compressed data will take up, keep stream position. + // At this point, it is not yet known how much bytes the compressed data will take up, keep stream position. long pixelDataSizePos = stream.Position; Span pixelRowSpan = pixels.DangerousGetRowSpan(y); stream.Position = pixelDataSizePos + 4; From f9df91108a8600696e0b1fa700fbe355c4a5a09f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 23 Mar 2026 18:14:00 +0100 Subject: [PATCH 41/66] Add support for writing exr with zip compression with 16 rows per block --- .../Compressors/ZipExrCompressor.cs | 6 +- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 4 +- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 78 +++++++++++-------- 3 files changed, 50 insertions(+), 38 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs index 6b82b70777..8a7b784601 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -29,8 +29,8 @@ public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerB public override uint CompressRowBlock(Span rows, int rowCount) { // Re-oder pixel values. - int n = rows.Length; - Span reordered = this.buffer.GetSpan(); + Span reordered = this.buffer.GetSpan()[..(int)(rowCount * this.BytesPerRow)]; + int n = reordered.Length; int t1 = 0; int t2 = (n + 1) >> 1; for (int i = 0; i < n; i++) @@ -42,7 +42,7 @@ public override uint CompressRowBlock(Span rows, int rowCount) // Predictor. Span predicted = reordered; byte p = predicted[0]; - for (int i = 1; i < rows.Length; i++) + for (int i = 1; i < predicted.Length; i++) { int d = (predicted[i] - p + 128 + 256) & 255; p = predicted[i]; diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index dbc6b7bf71..f8eb17f4e8 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -142,7 +142,7 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); - Span redPixelData = rowBuffer.GetSpan().Slice(0, width); + Span redPixelData = rowBuffer.GetSpan()[..width]; Span greenPixelData = rowBuffer.GetSpan().Slice(width, width); Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); @@ -195,7 +195,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); - Span redPixelData = rowBuffer.GetSpan().Slice(0, width); + Span redPixelData = rowBuffer.GetSpan()[..width]; Span greenPixelData = rowBuffer.GetSpan().Slice(width, width); Span bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width); Span alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width); diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index a7b37eda28..a89caf07eb 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -172,41 +172,47 @@ private ulong[] EncodeFloatingPointPixelData( using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow); ulong[] rowOffsets = new ulong[height]; - for (int y = 0; y < height; y++) + for (uint y = 0; y < height; y += rowsPerBlock) { rowOffsets[y] = (ulong)stream.Position; // Write row index. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y); stream.Write(this.buffer.AsSpan(0, 4)); // At this point, it is not yet known how much bytes the compressed data will take up, keep stream position. long pixelDataSizePos = stream.Position; - Span pixelRowSpan = pixels.DangerousGetRowSpan(y); stream.Position = pixelDataSizePos + 4; - for (int x = 0; x < width; x++) + uint rowsInBlockCount = 0; + for (uint rowIndex = y; rowIndex < y + rowsPerBlock && rowIndex < height; rowIndex++) { - Vector4 vector4 = pixelRowSpan[x].ToVector4(); - redBuffer[x] = vector4.X; - greenBuffer[x] = vector4.Y; - blueBuffer[x] = vector4.Z; - } + Span pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex); + for (int x = 0; x < width; x++) + { + Vector4 vector4 = pixelRowSpan[x].ToVector4(); + redBuffer[x] = vector4.X; + greenBuffer[x] = vector4.Y; + blueBuffer[x] = vector4.Z; + } - // Write pixel data to buffer. - Span rowBlockSpan = rowBlockBuffer.GetSpan(); - switch (this.pixelType) - { - case ExrPixelType.Float: - this.WriteSingleRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer); - break; - case ExrPixelType.Half: - this.WriteHalfSingleRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer); - break; + // Write pixel data to row block buffer. + Span rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow); + switch (this.pixelType) + { + case ExrPixelType.Float: + this.WriteSingleRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer); + break; + case ExrPixelType.Half: + this.WriteHalfSingleRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer); + break; + } + + rowsInBlockCount++; } // Write compressed pixel row data to the stream. - uint compressedBytes = compressor.CompressRowBlock(rowBlockSpan, (int)rowsPerBlock); + uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), (int)rowsInBlockCount); long positionAfterPixelData = stream.Position; // Write pixel row data size. @@ -243,34 +249,40 @@ private ulong[] EncodeUnsignedIntPixelData( Rgb96 rgb = default; ulong[] rowOffsets = new ulong[height]; - for (int y = 0; y < height; y++) + for (uint y = 0; y < height; y += rowsPerBlock) { rowOffsets[y] = (ulong)stream.Position; // Write row index. - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, (uint)y); + BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, y); stream.Write(this.buffer.AsSpan(0, 4)); // At this point, it is not yet known how much bytes the compressed data will take up, keep stream position. long pixelDataSizePos = stream.Position; - Span pixelRowSpan = pixels.DangerousGetRowSpan(y); stream.Position = pixelDataSizePos + 4; - for (int x = 0; x < width; x++) + uint rowsInBlockCount = 0; + for (uint rowIndex = y; rowIndex < y + rowsPerBlock && rowIndex < height; rowIndex++) { - Vector4 vector4 = pixelRowSpan[x].ToVector4(); - Rgb96.FromVector4(vector4); + Span pixelRowSpan = pixels.DangerousGetRowSpan((int)rowIndex); + for (int x = 0; x < width; x++) + { + Vector4 vector4 = pixelRowSpan[x].ToVector4(); + Rgb96.FromVector4(vector4); - redBuffer[x] = rgb.R; - greenBuffer[x] = rgb.G; - blueBuffer[x] = rgb.B; - } + redBuffer[x] = rgb.R; + greenBuffer[x] = rgb.G; + blueBuffer[x] = rgb.B; + } - // Write row data to buffer. - this.WriteUnsignedIntRow(rowBlockBuffer.GetSpan(), width, blueBuffer, greenBuffer, redBuffer); + // Write row data to row block buffer. + Span rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow); + this.WriteUnsignedIntRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer); + rowsInBlockCount++; + } // Write pixel row data compressed to the stream. - uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), 1); + uint compressedBytes = compressor.CompressRowBlock(rowBlockBuffer.GetSpan(), (int)rowsInBlockCount); long positionAfterPixelData = stream.Position; // Write pixel row data size. From a3349efb34ae27ccc3b35fa632a66218c4c16fea Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 24 Mar 2026 11:31:07 +0100 Subject: [PATCH 42/66] Add ExrEncoder tests --- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 2 +- .../Formats/Exr/ExrEncoderTests.cs | 58 +++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index a89caf07eb..e393dcf686 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -165,7 +165,7 @@ private ulong[] EncodeFloatingPointPixelData( using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3, AllocationOptions.Clean); using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); - Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); + Span redBuffer = rgbBuffer.GetSpan()[..width]; Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs new file mode 100644 index 0000000000..b16c925f8a --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Exr; +using SixLabors.ImageSharp.Formats.Exr.Constants; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + +namespace SixLabors.ImageSharp.Tests.Formats.Exr; + +[Trait("Format", "Exr")] +[ValidateDisposedMemoryAllocations] +public class ExrEncoderTests +{ + protected static readonly IImageDecoder ReferenceDecoder = new MagickReferenceDecoder(ExrFormat.Instance); + + [Theory] + [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] + public void ExrEncoder_WithNoCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestExrEncoderCore(provider, "NoCompression", compression: ExrCompression.None, imageDecoder: ExrDecoder.Instance); + + [Theory] + [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] + public void ExrEncoder_WithZipCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestExrEncoderCore(provider, "ZipCompression", compression: ExrCompression.Zip, imageDecoder: ExrDecoder.Instance); + + [Theory] + [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] + public void ExrEncoder_WithZipsCompression_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel => TestExrEncoderCore(provider, "ZipsCompression", compression: ExrCompression.Zips, imageDecoder: ExrDecoder.Instance); + + protected static void TestExrEncoderCore( + TestImageProvider provider, + object testOutputDetails, + ExrCompression compression = ExrCompression.None, + bool useExactComparer = true, + float compareTolerance = 0.001f, + IImageDecoder imageDecoder = null) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(); + ExrEncoder encoder = new() + { + Compression = compression, + }; + + // Does DebugSave & load reference CompareToReferenceInput(): + image.VerifyEncoder( + provider, + "exr", + testOutputDetails: testOutputDetails, + encoder: encoder, + customComparer: useExactComparer ? ImageComparer.Exact : ImageComparer.Tolerant(compareTolerance), + referenceDecoder: imageDecoder ?? ReferenceDecoder); + } +} From 9e80da911788e32a050c8cdeaf1737f9515a5932 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 24 Mar 2026 11:43:20 +0100 Subject: [PATCH 43/66] Try fix issue with undisposed buffers --- .../Exr/Compression/Compressors/ZipExrCompressor.cs | 9 +++++++-- .../Exr/Compression/Decompressors/ZipExrCompression.cs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs index 8a7b784601..587ecb7562 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -11,7 +11,7 @@ internal class ZipExrCompressor : ExrBaseCompressor { private readonly DeflateCompressionLevel compressionLevel; - private MemoryStream memoryStream = new(); + private readonly MemoryStream memoryStream = new(); private readonly System.Buffers.IMemoryOwner buffer; @@ -60,12 +60,17 @@ public override uint CompressRowBlock(Span rows, int rowCount) byte[] buffer = this.memoryStream.GetBuffer(); this.Output.Write(buffer, 0, size); - this.memoryStream = new(); + // Reset memory stream for next pixel row. + this.memoryStream.Seek(0, SeekOrigin.Begin); + this.memoryStream.SetLength(0); + return (uint)size; } /// protected override void Dispose(bool disposing) { + this.buffer.Dispose(); + this.memoryStream?.Dispose(); } } diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs index 5424634523..b8dc5efa8e 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs @@ -30,7 +30,7 @@ public override void Decompress(BufferedReadStream stream, uint compressedBytes, return left > 0 ? left : 0; }); inflateStream.AllocateNewBytes((int)this.BytesPerBlock, true); - DeflateStream dataStream = inflateStream.CompressedStream!; + using DeflateStream dataStream = inflateStream.CompressedStream!; int totalRead = 0; while (totalRead < buffer.Length) From d409a3b7d355eec168806b4a530fed6e51d59afe Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 24 Mar 2026 12:21:14 +0100 Subject: [PATCH 44/66] Use magick Reference decoder --- .../Formats/Exr/Compression/Compressors/ZipExrCompressor.cs | 3 ++- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 2 +- tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs index 587ecb7562..dcfbe3ae05 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -11,7 +11,7 @@ internal class ZipExrCompressor : ExrBaseCompressor { private readonly DeflateCompressionLevel compressionLevel; - private readonly MemoryStream memoryStream = new(); + private readonly MemoryStream memoryStream; private readonly System.Buffers.IMemoryOwner buffer; @@ -20,6 +20,7 @@ public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerB { this.compressionLevel = compressionLevel; this.buffer = allocator.Allocate((int)bytesPerBlock); + this.memoryStream = new(); } /// diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index f8eb17f4e8..6773f3b631 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -53,7 +53,7 @@ public ExrDecoderCore(ExrDecoderOptions options) : base(options.GeneralOptions) { this.configuration = options.GeneralOptions.Configuration; - this.memoryAllocator = this.configuration.MemoryAllocator; + this.memoryAllocator = this.configuration.MemoryAllocator; } /// diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs index b16c925f8a..6aa3ebc70b 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrEncoderTests.cs @@ -19,17 +19,17 @@ public class ExrEncoderTests [Theory] [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] public void ExrEncoder_WithNoCompression_Works(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestExrEncoderCore(provider, "NoCompression", compression: ExrCompression.None, imageDecoder: ExrDecoder.Instance); + where TPixel : unmanaged, IPixel => TestExrEncoderCore(provider, "NoCompression", compression: ExrCompression.None); [Theory] [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] public void ExrEncoder_WithZipCompression_Works(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestExrEncoderCore(provider, "ZipCompression", compression: ExrCompression.Zip, imageDecoder: ExrDecoder.Instance); + where TPixel : unmanaged, IPixel => TestExrEncoderCore(provider, "ZipCompression", compression: ExrCompression.Zip); [Theory] [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] public void ExrEncoder_WithZipsCompression_Works(TestImageProvider provider) - where TPixel : unmanaged, IPixel => TestExrEncoderCore(provider, "ZipsCompression", compression: ExrCompression.Zips, imageDecoder: ExrDecoder.Instance); + where TPixel : unmanaged, IPixel => TestExrEncoderCore(provider, "ZipsCompression", compression: ExrCompression.Zips); protected static void TestExrEncoderCore( TestImageProvider provider, From d5f4e23f01bc847a14f027b9f7cf99f16ba8b036 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 25 Mar 2026 17:18:56 +0100 Subject: [PATCH 45/66] Avoid code duplication for CalculateBytesPerRow and RowsPerBlock --- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 50 ++------------ src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 70 ++++---------------- src/ImageSharp/Formats/Exr/ExrUtils.cs | 41 ++++++++++++ 3 files changed, 58 insertions(+), 103 deletions(-) create mode 100644 src/ImageSharp/Formats/Exr/ExrUtils.cs diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 6773f3b631..44db67b618 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -132,8 +132,8 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf where TPixel : unmanaged, IPixel { bool hasAlpha = this.HasAlpha(); - uint bytesPerRow = this.CalculateBytesPerRow((uint)this.Width); - uint rowsPerBlock = this.RowsPerBlock(); + uint bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width); + uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression); uint bytesPerBlock = bytesPerRow * rowsPerBlock; int width = this.Width; int height = this.Height; @@ -185,8 +185,8 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe where TPixel : unmanaged, IPixel { bool hasAlpha = this.HasAlpha(); - uint bytesPerRow = this.CalculateBytesPerRow((uint)this.Width); - uint rowsPerBlock = this.RowsPerBlock(); + uint bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width); + uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression); uint bytesPerBlock = bytesPerRow * rowsPerBlock; int width = this.Width; int height = this.Height; @@ -728,48 +728,6 @@ private bool HasAlpha() return false; } - private uint CalculateBytesPerRow(uint width) - { - uint bytesPerRow = 0; - foreach (ExrChannelInfo channelInfo in this.Channels) - { - if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal) - || channelInfo.ChannelName.Equals("R", StringComparison.Ordinal) - || channelInfo.ChannelName.Equals("G", StringComparison.Ordinal) - || channelInfo.ChannelName.Equals("B", StringComparison.Ordinal) - || channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal)) - { - if (channelInfo.PixelType == ExrPixelType.Half) - { - bytesPerRow += 2 * width; - } - else - { - bytesPerRow += 4 * width; - } - } - } - - return bytesPerRow; - } - - private uint RowsPerBlock() - { - switch (this.Compression) - { - case ExrCompression.Zip: - case ExrCompression.Pxr24: - return 16; - case ExrCompression.B44: - case ExrCompression.B44A: - case ExrCompression.Piz: - return 32; - - default: - return 1; - } - } - private ulong ReadUnsignedLong(BufferedReadStream stream) { int bytesRead = stream.Read(this.buffer, 0, 8); diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index e393dcf686..2705a2057c 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -158,8 +158,8 @@ private ulong[] EncodeFloatingPointPixelData( ExrCompression compression) where TPixel : unmanaged, IPixel { - uint bytesPerRow = this.CalculateBytesPerRow(channels, (uint)width); - uint rowsPerBlock = this.RowsPerBlock(compression); + uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width); + uint rowsPerBlock = ExrUtils.RowsPerBlock(compression); uint bytesPerBlock = bytesPerRow * rowsPerBlock; int channelCount = channels.Count; @@ -234,8 +234,8 @@ private ulong[] EncodeUnsignedIntPixelData( ExrCompression compression) where TPixel : unmanaged, IPixel { - uint bytesPerRow = this.CalculateBytesPerRow(channels, (uint)width); - uint rowsPerBlock = this.RowsPerBlock(compression); + uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width); + uint rowsPerBlock = ExrUtils.RowsPerBlock(compression); uint bytesPerBlock = bytesPerRow * rowsPerBlock; int channelCount = channels.Count; @@ -335,19 +335,19 @@ private void WriteHalfSingleRow(Span buffer, int width, Span blueBu int offset = 0; for (int x = 0; x < width; x++) { - this.WriteHalfSingle(buffer.Slice(offset), blueBuffer[x]); + WriteHalfSingle(buffer.Slice(offset), blueBuffer[x]); offset += 2; } for (int x = 0; x < width; x++) { - this.WriteHalfSingle(buffer.Slice(offset), greenBuffer[x]); + WriteHalfSingle(buffer.Slice(offset), greenBuffer[x]); offset += 2; } for (int x = 0; x < width; x++) { - this.WriteHalfSingle(buffer.Slice(offset), redBuffer[x]); + WriteHalfSingle(buffer.Slice(offset), redBuffer[x]); offset += 2; } } @@ -357,19 +357,19 @@ private void WriteUnsignedIntRow(Span buffer, int width, Span blueBu int offset = 0; for (int x = 0; x < width; x++) { - this.WriteUnsignedInt(buffer.Slice(offset), blueBuffer[x]); + WriteUnsignedInt(buffer.Slice(offset), blueBuffer[x]); offset += 4; } for (int x = 0; x < width; x++) { - this.WriteUnsignedInt(buffer.Slice(offset), greenBuffer[x]); + WriteUnsignedInt(buffer.Slice(offset), greenBuffer[x]); offset += 4; } for (int x = 0; x < width; x++) { - this.WriteUnsignedInt(buffer.Slice(offset), redBuffer[x]); + WriteUnsignedInt(buffer.Slice(offset), redBuffer[x]); offset += 4; } } @@ -521,10 +521,10 @@ private unsafe void WriteSingle(Stream stream, float value) private unsafe void WriteSingle(Span buffer, float value) => BinaryPrimitives.WriteInt32LittleEndian(buffer, *(int*)&value); [MethodImpl(InliningOptions.ShortMethod)] - private void WriteHalfSingle(Span buffer, float value) + private static void WriteHalfSingle(Span buffer, float value) { ushort valueAsShort = HalfTypeHelper.Pack(value); - BinaryPrimitives.WriteUInt16LittleEndian(this.buffer, valueAsShort); + BinaryPrimitives.WriteUInt16LittleEndian(buffer, valueAsShort); } [MethodImpl(InliningOptions.ShortMethod)] @@ -535,49 +535,5 @@ private void WriteUnsignedInt(Stream stream, uint value) } [MethodImpl(InliningOptions.ShortMethod)] - private void WriteUnsignedInt(Span buffer, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); - - // TODO: avoid code duplication: This code is duplicate in the decoder. - private uint CalculateBytesPerRow(List channels, uint width) - { - uint bytesPerRow = 0; - foreach (ExrChannelInfo channelInfo in channels) - { - if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal) - || channelInfo.ChannelName.Equals("R", StringComparison.Ordinal) - || channelInfo.ChannelName.Equals("G", StringComparison.Ordinal) - || channelInfo.ChannelName.Equals("B", StringComparison.Ordinal) - || channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal)) - { - if (channelInfo.PixelType == ExrPixelType.Half) - { - bytesPerRow += 2 * width; - } - else - { - bytesPerRow += 4 * width; - } - } - } - - return bytesPerRow; - } - - // TODO: avoid code duplication: This code is duplicate in the decoder. - private uint RowsPerBlock(ExrCompression compression) - { - switch (compression) - { - case ExrCompression.Zip: - case ExrCompression.Pxr24: - return 16; - case ExrCompression.B44: - case ExrCompression.B44A: - case ExrCompression.Piz: - return 32; - - default: - return 1; - } - } + private static void WriteUnsignedInt(Span buffer, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); } diff --git a/src/ImageSharp/Formats/Exr/ExrUtils.cs b/src/ImageSharp/Formats/Exr/ExrUtils.cs new file mode 100644 index 0000000000..4d172c6ef8 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/ExrUtils.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Exr.Constants; + +namespace SixLabors.ImageSharp.Formats.Exr; + +internal static class ExrUtils +{ + public static uint CalculateBytesPerRow(IList channels, uint width) + { + uint bytesPerRow = 0; + foreach (ExrChannelInfo channelInfo in channels) + { + if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("R", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("G", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("B", StringComparison.Ordinal) + || channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal)) + { + if (channelInfo.PixelType == ExrPixelType.Half) + { + bytesPerRow += 2 * width; + } + else + { + bytesPerRow += 4 * width; + } + } + } + + return bytesPerRow; + } + + public static uint RowsPerBlock(ExrCompression compression) => compression switch + { + ExrCompression.Zip or ExrCompression.Pxr24 => 16, + ExrCompression.B44 or ExrCompression.B44A or ExrCompression.Piz => 32, + _ => 1, + }; +} From 7cc73d9c15d452029cad81865b777c0e670b37c0 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 25 Mar 2026 19:54:37 +0100 Subject: [PATCH 46/66] Fix issue decoding last row block with zip compression --- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 14 +++++-- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 43 ++++++++------------ 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 44db67b618..52a08e495d 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -149,7 +149,8 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, width, bytesPerBlock, bytesPerRow, rowsPerBlock, channelCount); - for (uint y = 0; y < height; y += rowsPerBlock) + int decodedRows = 0; + while (decodedRows < height) { ulong rowOffset = this.ReadUnsignedLong(stream); long nextRowOffsetPosition = stream.Position; @@ -175,6 +176,8 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf HalfVector4 pixelValue = new(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : 1.0f); pixelRow[x] = TPixel.FromVector4(pixelValue.ToVector4()); } + + decodedRows++; } stream.Position = nextRowOffsetPosition; @@ -202,7 +205,8 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, width, bytesPerBlock, bytesPerRow, rowsPerBlock, channelCount); - for (uint y = 0; y < height; y += rowsPerBlock) + int decodedRows = 0; + while (decodedRows < height) { ulong rowOffset = this.ReadUnsignedLong(stream); long nextRowOffsetPosition = stream.Position; @@ -223,13 +227,15 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe offset += this.ReadUnsignedIntChannelData(stream, channel, decompressedPixelData.Slice(offset), redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); } - stream.Position = nextRowOffsetPosition; - for (int x = 0; x < width; x++) { pixelRow[x] = ColorScaleTo32Bit(redPixelData[x], greenPixelData[x], bluePixelData[x], hasAlpha ? alphaPixelData[x] : uint.MaxValue); } + + decodedRows++; } + + stream.Position = nextRowOffsetPosition; } } diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index 2705a2057c..ff606b677a 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -201,10 +201,10 @@ private ulong[] EncodeFloatingPointPixelData( switch (this.pixelType) { case ExrPixelType.Float: - this.WriteSingleRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer); + WriteSingleRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer); break; case ExrPixelType.Half: - this.WriteHalfSingleRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer); + WriteHalfSingleRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer); break; } @@ -277,7 +277,7 @@ private ulong[] EncodeUnsignedIntPixelData( // Write row data to row block buffer. Span rowBlockSpan = rowBlockBuffer.GetSpan().Slice((int)(rowsInBlockCount * bytesPerRow), (int)bytesPerRow); - this.WriteUnsignedIntRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer); + WriteUnsignedIntRow(rowBlockSpan, width, blueBuffer, greenBuffer, redBuffer); rowsInBlockCount++; } @@ -308,68 +308,68 @@ private void WriteHeader(Stream stream, ExrHeaderAttributes header) stream.WriteByte(0); } - private void WriteSingleRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + private static void WriteSingleRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) { int offset = 0; for (int x = 0; x < width; x++) { - this.WriteSingle(buffer.Slice(offset), blueBuffer[x]); + WriteSingleToBuffer(buffer.Slice(offset, 4), blueBuffer[x]); offset += 4; } for (int x = 0; x < width; x++) { - this.WriteSingle(buffer.Slice(offset), greenBuffer[x]); + WriteSingleToBuffer(buffer.Slice(offset, 4), greenBuffer[x]); offset += 4; } for (int x = 0; x < width; x++) { - this.WriteSingle(buffer.Slice(offset), redBuffer[x]); + WriteSingleToBuffer(buffer.Slice(offset, 4), redBuffer[x]); offset += 4; } } - private void WriteHalfSingleRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + private static void WriteHalfSingleRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) { int offset = 0; for (int x = 0; x < width; x++) { - WriteHalfSingle(buffer.Slice(offset), blueBuffer[x]); + WriteHalfSingleToBuffer(buffer.Slice(offset, 2), blueBuffer[x]); offset += 2; } for (int x = 0; x < width; x++) { - WriteHalfSingle(buffer.Slice(offset), greenBuffer[x]); + WriteHalfSingleToBuffer(buffer.Slice(offset, 2), greenBuffer[x]); offset += 2; } for (int x = 0; x < width; x++) { - WriteHalfSingle(buffer.Slice(offset), redBuffer[x]); + WriteHalfSingleToBuffer(buffer.Slice(offset, 2), redBuffer[x]); offset += 2; } } - private void WriteUnsignedIntRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) + private static void WriteUnsignedIntRow(Span buffer, int width, Span blueBuffer, Span greenBuffer, Span redBuffer) { int offset = 0; for (int x = 0; x < width; x++) { - WriteUnsignedInt(buffer.Slice(offset), blueBuffer[x]); + WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), blueBuffer[x]); offset += 4; } for (int x = 0; x < width; x++) { - WriteUnsignedInt(buffer.Slice(offset), greenBuffer[x]); + WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), greenBuffer[x]); offset += 4; } for (int x = 0; x < width; x++) { - WriteUnsignedInt(buffer.Slice(offset), redBuffer[x]); + WriteUnsignedIntToBuffer(buffer.Slice(offset, 4), redBuffer[x]); offset += 4; } } @@ -518,22 +518,15 @@ private unsafe void WriteSingle(Stream stream, float value) } [MethodImpl(InliningOptions.ShortMethod)] - private unsafe void WriteSingle(Span buffer, float value) => BinaryPrimitives.WriteInt32LittleEndian(buffer, *(int*)&value); + private static unsafe void WriteSingleToBuffer(Span buffer, float value) => BinaryPrimitives.WriteInt32LittleEndian(buffer, *(int*)&value); [MethodImpl(InliningOptions.ShortMethod)] - private static void WriteHalfSingle(Span buffer, float value) + private static void WriteHalfSingleToBuffer(Span buffer, float value) { ushort valueAsShort = HalfTypeHelper.Pack(value); BinaryPrimitives.WriteUInt16LittleEndian(buffer, valueAsShort); } [MethodImpl(InliningOptions.ShortMethod)] - private void WriteUnsignedInt(Stream stream, uint value) - { - BinaryPrimitives.WriteUInt32LittleEndian(this.buffer, value); - stream.Write(this.buffer.AsSpan(0, 4)); - } - - [MethodImpl(InliningOptions.ShortMethod)] - private static void WriteUnsignedInt(Span buffer, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); + private static void WriteUnsignedIntToBuffer(Span buffer, uint value) => BinaryPrimitives.WriteUInt32LittleEndian(buffer, value); } From 6a92b0cd442c34b220019ddda8928a9038dee7ab Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 26 Mar 2026 17:35:57 +0100 Subject: [PATCH 47/66] Implement missing Rgb96 methods --- .../Common/Helpers/ColorNumerics.cs | 11 ++ .../PixelImplementations/Rgb96.cs | 100 +++++++++++++----- .../PixelImplementations/Rgba32.cs | 13 +++ 3 files changed, 97 insertions(+), 27 deletions(-) diff --git a/src/ImageSharp/Common/Helpers/ColorNumerics.cs b/src/ImageSharp/Common/Helpers/ColorNumerics.cs index 1c30d857f6..7a0e6d47ab 100644 --- a/src/ImageSharp/Common/Helpers/ColorNumerics.cs +++ b/src/ImageSharp/Common/Helpers/ColorNumerics.cs @@ -12,6 +12,8 @@ namespace SixLabors.ImageSharp; /// internal static class ColorNumerics { + private const float Scale32Bit = 1f / 0xFFFFFFFF; + /// /// Vector for converting pixel to gray value as specified by /// ITU-R Recommendation BT.709. @@ -132,6 +134,15 @@ public static byte From16BitTo8Bit(ushort component) => // (V * 255 + 32895) >> 16 (byte)(((component * 255) + 32895) >> 16); + /// + /// Scales a value from an 32 bit to + /// an 8 bit equivalent. + /// + /// The 32 bit component value. + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte From32BitTo8Bit(uint component) => (byte)(component * Scale32Bit); + /// /// Scales a value from an 8 bit to /// an 16 bit equivalent. diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs index 165f13b07e..6237d3d374 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.PixelFormats; /// /// [StructLayout(LayoutKind.Sequential)] -public partial struct Rgb96 : IPixel +public partial struct Rgb96 : IPixel, IEquatable { private const float InvMax = 1.0f / uint.MaxValue; @@ -74,60 +74,106 @@ public Rgb96(uint r, uint g, uint b) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ToScaledVector4() => this.ToVector4(); + public readonly Vector4 ToScaledVector4() => this.ToVector4(); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ToVector4() => new( + public readonly Vector4 ToVector4() => new( this.R * InvMax, this.G * InvMax, this.B * InvMax, 1.0f); - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); + public static PixelOperations CreatePixelOperations() => new(); - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Equals(Rgb96 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); + public static Rgb96 FromScaledVector4(Vector4 source) => FromVector4(source); - /// - public override string ToString() => FormattableString.Invariant($"Rgb96({this.R}, {this.G}, {this.B})"); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromVector4(Vector4 source) => FromVector4(source); - public static PixelOperations CreatePixelOperations() => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromAbgr32(Abgr32 source) => new(source.R, source.G, source.B); - public static Rgb96 FromScaledVector4(Vector4 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromArgb32(Argb32 source) => new(source.R, source.G, source.B); - public static Rgb96 FromVector4(Vector4 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); - public static Rgb96 FromAbgr32(Abgr32 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromBgr24(Bgr24 source) => new(source.R, source.G, source.B); - public static Rgb96 FromArgb32(Argb32 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromBgra32(Bgra32 source) => new(source.R, source.G, source.B); - public static Rgb96 FromBgra5551(Bgra5551 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromL8(L8 source) + { + ushort rgb = ColorNumerics.From8BitTo16Bit(source.PackedValue); + return new Rgb96(rgb, rgb, rgb); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromL16(L16 source) => new(source.PackedValue, source.PackedValue, source.PackedValue); - public static Rgb96 FromBgr24(Bgr24 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromLa16(La16 source) => new(source.PackedValue, source.PackedValue, source.PackedValue); - public static Rgb96 FromBgra32(Bgra32 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromLa32(La32 source) => new(source.L, source.L, source.L); - public static Rgb96 FromL8(L8 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromRgb24(Rgb24 source) => new(source.R, source.G, source.B); - public static Rgb96 FromL16(L16 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromRgba32(Rgba32 source) => new(source.R, source.G, source.B); - public static Rgb96 FromLa16(La16 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromRgb48(Rgb48 source) => new(source.R, source.G, source.B); - public static Rgb96 FromLa32(La32 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgb96 FromRgba64(Rgba64 source) => new(source.R, source.G, source.B); - public static Rgb96 FromRgb24(Rgb24 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 16, 16, 16), + PixelColorType.RGB, + PixelAlphaRepresentation.None); - public static Rgb96 FromRgba32(Rgba32 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Rgba32 ToRgba32() => Rgba32.FromRgb96(this); - public static Rgb96 FromRgb48(Rgb48 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); - public static Rgb96 FromRgba64(Rgba64 source) => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(Rgb96 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); - public static PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); + /// + public override readonly string ToString() => FormattableString.Invariant($"Rgb96({this.R}, {this.G}, {this.B})"); - public Rgba32 ToRgba32() => throw new NotImplementedException(); + /// + public override readonly bool Equals(object? obj) => obj is Rgb96 rgb && rgb.R == this.R && rgb.G == this.G && rgb.B == this.B; } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs index 199754c690..34a7f67871 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs @@ -314,6 +314,19 @@ public static Rgba32 FromRgba64(Rgba64 source) A = ColorNumerics.From16BitTo8Bit(source.A) }; + /// + /// Initializes the pixel instance from an value. + /// + /// The value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Rgba32 FromRgb96(Rgb96 source) + => new() + { + R = ColorNumerics.From32BitTo8Bit(source.R), + G = ColorNumerics.From32BitTo8Bit(source.G), + B = ColorNumerics.From32BitTo8Bit(source.B) + }; + /// /// Converts the value of this instance to a hexadecimal string. /// From 355d1bc6b6c64c02ecb6d40f62aa23d97475dbd9 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 26 Mar 2026 18:29:28 +0100 Subject: [PATCH 48/66] Implement missing methods for Rgba128 --- .../PixelImplementations/Rgb96.cs | 10 +-- .../PixelImplementations/Rgba128.cs | 87 ++++++++++++------- 2 files changed, 63 insertions(+), 34 deletions(-) diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs index 6237d3d374..d318b067c1 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgb96.cs @@ -155,7 +155,7 @@ public static Rgb96 FromL8(L8 source) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create( - PixelComponentInfo.Create(4, 16, 16, 16), + PixelComponentInfo.Create(3, 32, 32, 32), PixelColorType.RGB, PixelAlphaRepresentation.None); @@ -167,13 +167,13 @@ public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create( [MethodImpl(MethodImplOptions.AggressiveInlining)] public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public readonly bool Equals(Rgb96 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); - /// public override readonly string ToString() => FormattableString.Invariant($"Rgb96({this.R}, {this.G}, {this.B})"); /// public override readonly bool Equals(object? obj) => obj is Rgb96 rgb && rgb.R == this.R && rgb.G == this.G && rgb.B == this.B; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(Rgb96 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B); } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs index fd094be599..c748456b87 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba128.cs @@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.PixelFormats; /// /// [StructLayout(LayoutKind.Sequential)] -public partial struct Rgba128 : IPixel +public partial struct Rgba128 : IPixel, IEquatable { private const float InvMax = 1.0f / uint.MaxValue; @@ -81,58 +81,87 @@ public Rgba128(uint r, uint g, uint b, uint a) /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Vector4 ToVector4() => new( + public readonly Vector4 ToVector4() => new( this.R * InvMax, this.G * InvMax, this.B * InvMax, this.A * InvMax); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override int GetHashCode() => HashCode.Combine(this.R, this.G, this.B, this.A); + /// + public static PixelOperations CreatePixelOperations() => new(); - /// - [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 static Rgba128 FromScaledVector4(Vector4 source) => FromVector4(source); - /// - public override string ToString() => FormattableString.Invariant($"Rgba128({this.R}, {this.G}, {this.B}, {this.A})"); + /// + public static Rgba128 FromVector4(Vector4 source) => FromVector4(source); - public static PixelOperations CreatePixelOperations() => throw new NotImplementedException(); + /// + public static Rgba128 FromAbgr32(Abgr32 source) => new(source.R, source.G, source.B, source.A); - public static Rgba128 FromScaledVector4(Vector4 source) => throw new NotImplementedException(); + /// + public static Rgba128 FromArgb32(Argb32 source) => new(source.R, source.G, source.B, source.A); - public static Rgba128 FromVector4(Vector4 source) => throw new NotImplementedException(); + /// + public static Rgba128 FromBgra5551(Bgra5551 source) => FromScaledVector4(source.ToScaledVector4()); - public static Rgba128 FromAbgr32(Abgr32 source) => throw new NotImplementedException(); + /// + public static Rgba128 FromBgr24(Bgr24 source) => new(source.R, source.G, source.B, uint.MaxValue); - public static Rgba128 FromArgb32(Argb32 source) => throw new NotImplementedException(); + /// + public static Rgba128 FromBgra32(Bgra32 source) => new(source.R, source.G, source.B, source.A); - public static Rgba128 FromBgra5551(Bgra5551 source) => throw new NotImplementedException(); + /// + public static Rgba128 FromL8(L8 source) + { + ushort rgb = ColorNumerics.From8BitTo16Bit(source.PackedValue); + return new Rgba128(rgb, rgb, rgb, rgb); + } - public static Rgba128 FromBgr24(Bgr24 source) => throw new NotImplementedException(); + /// + public static Rgba128 FromL16(L16 source) => new(source.PackedValue, source.PackedValue, source.PackedValue, source.PackedValue); - public static Rgba128 FromBgra32(Bgra32 source) => throw new NotImplementedException(); + /// + public static Rgba128 FromLa16(La16 source) => new(source.PackedValue, source.PackedValue, source.PackedValue, source.PackedValue); - public static Rgba128 FromL8(L8 source) => throw new NotImplementedException(); + /// + public static Rgba128 FromLa32(La32 source) => new(source.L, source.L, source.L, source.L); - public static Rgba128 FromL16(L16 source) => throw new NotImplementedException(); + /// + public static Rgba128 FromRgb24(Rgb24 source) => new(source.R, source.G, source.B, uint.MaxValue); - public static Rgba128 FromLa16(La16 source) => throw new NotImplementedException(); + /// + public static Rgba128 FromRgba32(Rgba32 source) => new(source.R, source.G, source.B, source.A); - public static Rgba128 FromLa32(La32 source) => throw new NotImplementedException(); + /// + public static Rgba128 FromRgb48(Rgb48 source) => new(source.R, source.G, source.B, uint.MaxValue); - public static Rgba128 FromRgb24(Rgb24 source) => throw new NotImplementedException(); + /// + public static Rgba128 FromRgba64(Rgba64 source) => new(source.R, source.G, source.B, source.A); - public static Rgba128 FromRgba32(Rgba32 source) => throw new NotImplementedException(); + /// + public static PixelTypeInfo GetPixelTypeInfo() => PixelTypeInfo.Create( + PixelComponentInfo.Create(4, 32, 32, 32), + PixelColorType.RGB, + PixelAlphaRepresentation.Unassociated); - public static Rgba128 FromRgb48(Rgb48 source) => throw new NotImplementedException(); + /// + public readonly Rgba32 ToRgba32() => throw new NotImplementedException(); - public static Rgba128 FromRgba64(Rgba64 source) => throw new NotImplementedException(); + /// + public readonly Vector4 ToScaledVector4() => throw new NotImplementedException(); - public static PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override readonly int GetHashCode() => HashCode.Combine(this.R, this.G, this.B, this.A); + + /// + public override readonly string ToString() => FormattableString.Invariant($"Rgba128({this.R}, {this.G}, {this.B}, {this.A})"); - public Rgba32 ToRgba32() => throw new NotImplementedException(); + /// + public override readonly bool Equals(object? obj) => obj is Rgba128 rgb && rgb.R == this.R && rgb.G == this.G && rgb.B == this.B && rgb.A == this.A; - public Vector4 ToScaledVector4() => throw new NotImplementedException(); + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly bool Equals(Rgba128 other) => this.R.Equals(other.R) && this.G.Equals(other.G) && this.B.Equals(other.B) && this.A.Equals(other.A); } From c0e3d283d81ab4066b93ef74bcf3ce2bc3e25dea Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 26 Mar 2026 19:18:21 +0100 Subject: [PATCH 49/66] Use cancellation token, when decoding exr images --- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 14 +++++++++----- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 52a08e495d..555b07bacf 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -115,10 +115,10 @@ protected override Image Decode(BufferedReadStream stream, Cance { case ExrPixelType.Half: case ExrPixelType.Float: - this.DecodeFloatingPointPixelData(stream, pixels); + this.DecodeFloatingPointPixelData(stream, pixels, cancellationToken); break; case ExrPixelType.UnsignedInt: - this.DecodeUnsignedIntPixelData(stream, pixels); + this.DecodeUnsignedIntPixelData(stream, pixels, cancellationToken); break; default: ExrThrowHelper.ThrowNotSupported("Pixel type is not supported"); @@ -128,7 +128,7 @@ protected override Image Decode(BufferedReadStream stream, Cance return image; } - private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buffer2D pixels) + private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buffer2D pixels, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { bool hasAlpha = this.HasAlpha(); @@ -168,7 +168,7 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { ExrChannelInfo channel = this.Channels[channelIdx]; - offset += ReadFloatChannelData(stream, channel, decompressedPixelData.Slice(offset), redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); + offset += ReadFloatChannelData(stream, channel, decompressedPixelData[offset..], redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); } for (int x = 0; x < width; x++) @@ -181,10 +181,12 @@ private void DecodeFloatingPointPixelData(BufferedReadStream stream, Buf } stream.Position = nextRowOffsetPosition; + + cancellationToken.ThrowIfCancellationRequested(); } } - private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffer2D pixels) + private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffer2D pixels, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { bool hasAlpha = this.HasAlpha(); @@ -236,6 +238,8 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe } stream.Position = nextRowOffsetPosition; + + cancellationToken.ThrowIfCancellationRequested(); } } diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index ff606b677a..d6736597a9 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -241,7 +241,7 @@ private ulong[] EncodeUnsignedIntPixelData( using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3, AllocationOptions.Clean); using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); - Span redBuffer = rgbBuffer.GetSpan().Slice(0, width); + Span redBuffer = rgbBuffer.GetSpan()[..width]; Span greenBuffer = rgbBuffer.GetSpan().Slice(width, width); Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); From 3e7ef68dcaa16643e0db9d4738db6dc533331fbb Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 26 Mar 2026 19:50:51 +0100 Subject: [PATCH 50/66] Use cancellation token when encoding exr images --- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index d6736597a9..7a66dfac26 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -133,7 +133,7 @@ public void Encode(Image image, Stream stream, CancellationToken case ExrPixelType.Half: case ExrPixelType.Float: { - ulong[] rowOffsets = this.EncodeFloatingPointPixelData(stream, pixels, width, height, channels, this.Compression); + ulong[] rowOffsets = this.EncodeFloatingPointPixelData(stream, pixels, width, height, channels, this.Compression, cancellationToken); stream.Position = (long)startOfRowOffsetData; this.WriteRowOffsets(stream, height, rowOffsets); break; @@ -141,7 +141,7 @@ public void Encode(Image image, Stream stream, CancellationToken case ExrPixelType.UnsignedInt: { - ulong[] rowOffsets = this.EncodeUnsignedIntPixelData(stream, pixels, width, height, channels, this.Compression); + ulong[] rowOffsets = this.EncodeUnsignedIntPixelData(stream, pixels, width, height, channels, this.Compression, cancellationToken); stream.Position = (long)startOfRowOffsetData; this.WriteRowOffsets(stream, height, rowOffsets); break; @@ -155,7 +155,8 @@ private ulong[] EncodeFloatingPointPixelData( int width, int height, List channels, - ExrCompression compression) + ExrCompression compression, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width); @@ -220,6 +221,8 @@ private ulong[] EncodeFloatingPointPixelData( stream.Position = pixelDataSizePos; stream.Write(this.buffer.AsSpan(0, 4)); stream.Position = positionAfterPixelData; + + cancellationToken.ThrowIfCancellationRequested(); } return rowOffsets; @@ -231,7 +234,8 @@ private ulong[] EncodeUnsignedIntPixelData( int width, int height, List channels, - ExrCompression compression) + ExrCompression compression, + CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width); @@ -290,6 +294,8 @@ private ulong[] EncodeUnsignedIntPixelData( stream.Position = pixelDataSizePos; stream.Write(this.buffer.AsSpan(0, 4)); stream.Position = positionAfterPixelData; + + cancellationToken.ThrowIfCancellationRequested(); } return rowOffsets; From d29fe89a27e7f9faac9f5cd392e8fbd01d74ed72 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 27 Mar 2026 10:21:02 +0100 Subject: [PATCH 51/66] Add benchmark for decoding exr image --- src/ImageSharp/Configuration.cs | 2 +- src/ImageSharp/Formats/Exr/ExrDecoder.cs | 2 +- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 126 +++--------------- src/ImageSharp/Formats/Exr/ExrMetadata.cs | 5 + .../PixelImplementations/Rgba32.cs | 1 + .../Codecs/Exr/DecodeExr.cs | 68 ++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + .../Images/Input/Exr/Calliphora_benchmark.exr | 3 + 8 files changed, 96 insertions(+), 112 deletions(-) create mode 100644 tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs create mode 100644 tests/Images/Input/Exr/Calliphora_benchmark.exr diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 6e431ba31b..a7f84363e6 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -5,10 +5,10 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Cur; +using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Jpeg; -using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Qoi; diff --git a/src/ImageSharp/Formats/Exr/ExrDecoder.cs b/src/ImageSharp/Formats/Exr/ExrDecoder.cs index 11937eed4f..2e27717282 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoder.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoder.cs @@ -28,7 +28,7 @@ protected override ImageInfo Identify(DecoderOptions options, Stream stream, Can return new ExrDecoderCore(new ExrDecoderOptions { GeneralOptions = options }).Identify(options.Configuration, stream, cancellationToken); } - //// + /// protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 555b07bacf..4f3f113536 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -6,7 +6,6 @@ using System.Buffers.Binary; using System.Numerics; using System.Runtime.CompilerServices; -using System.Runtime.Intrinsics; using System.Text; using SixLabors.ImageSharp.Formats.Exr.Compression; using SixLabors.ImageSharp.Formats.Exr.Constants; @@ -23,7 +22,6 @@ namespace SixLabors.ImageSharp.Formats.Exr; internal sealed class ExrDecoderCore : ImageDecoderCore { private const float Scale32Bit = 1f / 0xFFFFFFFF; - private static readonly Vector4 Scale32BitVector = Vector128.Create(Scale32Bit, Scale32Bit, Scale32Bit, 1f).AsVector4(); /// /// Reusable buffer. @@ -56,11 +54,6 @@ public ExrDecoderCore(ExrDecoderOptions options) this.memoryAllocator = this.configuration.MemoryAllocator; } - /// - /// Gets the dimensions of the image. - /// - public Size Dimensions => new(this.Width, this.Height); - /// /// Gets or sets the image width. /// @@ -81,16 +74,6 @@ public ExrDecoderCore(ExrDecoderOptions options) /// private ExrCompression Compression { get; set; } - /// - /// Gets or sets the image data type, either RGB, RGBA or gray. - /// - private ExrImageDataType ImageDataType { get; set; } - - /// - /// Gets or sets the image type, either ScanLine or tiled. - /// - private ExrImageType ImageType { get; set; } - /// /// Gets or sets the header attributes. /// @@ -106,7 +89,6 @@ protected override Image Decode(BufferedReadStream stream, Cance } ExrPixelType pixelType = this.ValidateChannels(); - this.ReadImageDataType(); Image image = new(this.configuration, this.Width, this.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); @@ -226,7 +208,7 @@ private void DecodeUnsignedIntPixelData(BufferedReadStream stream, Buffe for (int channelIdx = 0; channelIdx < this.Channels.Count; channelIdx++) { ExrChannelInfo channel = this.Channels[channelIdx]; - offset += this.ReadUnsignedIntChannelData(stream, channel, decompressedPixelData.Slice(offset), redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); + offset += this.ReadUnsignedIntChannelData(stream, channel, decompressedPixelData[offset..], redPixelData, greenPixelData, bluePixelData, alphaPixelData, width); } for (int x = 0; x < width; x++) @@ -320,29 +302,18 @@ private int ReadUnsignedIntChannelData( } } - private static int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) + private static int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) => channel.PixelType switch { - switch (channel.PixelType) - { - case ExrPixelType.Half: - return ReadPixelRowChannelHalfSingle(decompressedPixelData, pixelData, width); - case ExrPixelType.Float: - return ReadPixelRowChannelSingle(decompressedPixelData, pixelData, width); - } - - return 0; - } + ExrPixelType.Half => ReadPixelRowChannelHalfSingle(decompressedPixelData, pixelData, width), + ExrPixelType.Float => ReadPixelRowChannelSingle(decompressedPixelData, pixelData, width), + _ => 0, + }; - private static int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) + private static int ReadChannelData(ExrChannelInfo channel, Span decompressedPixelData, Span pixelData, int width) => channel.PixelType switch { - switch (channel.PixelType) - { - case ExrPixelType.UnsignedInt: - return ReadPixelRowChannelUnsignedInt(decompressedPixelData, pixelData, width); - } - - return 0; - } + ExrPixelType.UnsignedInt => ReadPixelRowChannelUnsignedInt(decompressedPixelData, pixelData, width), + _ => 0, + }; private static int ReadPixelRowChannelHalfSingle(Span decompressedPixelData, Span channelData, int width) { @@ -442,7 +413,7 @@ private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) byte flagsByte2 = (byte)stream.ReadByte(); if ((flagsByte0 & (1 << 1)) != 0) { - this.ImageType = ExrImageType.Tiled; + ExrThrowHelper.ThrowNotSupported("Decoding tiled exr images is not supported yet!"); } this.HeaderAttributes = this.ParseHeaderAttributes(stream); @@ -560,7 +531,7 @@ private ExrBox2i ReadBoxInteger(BufferedReadStream stream) private List ReadChannelList(BufferedReadStream stream, int attributeSize) { - List channels = new(); + List channels = []; while (attributeSize > 1) { ExrChannelInfo channelInfo = this.ReadChannelInfo(stream, out int bytesRead); @@ -654,76 +625,11 @@ private ExrPixelType FindPixelType() return pixelType.Value; } - private bool IsSupportedCompression() - { - switch (this.Compression) - { - case ExrCompression.None: - case ExrCompression.Zip: - case ExrCompression.Zips: - case ExrCompression.RunLengthEncoded: - case ExrCompression.B44: - return true; - } - - return false; - } - - private void ReadImageDataType() + private bool IsSupportedCompression() => this.Compression switch { - bool hasRedChannel = false; - bool hasGreenChannel = false; - bool hasBlueChannel = false; - bool hasAlphaChannel = false; - bool hasLuminance = false; - foreach (ExrChannelInfo channelInfo in this.Channels) - { - if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal)) - { - hasAlphaChannel = true; - } - - if (channelInfo.ChannelName.Equals("R", StringComparison.Ordinal)) - { - hasRedChannel = true; - } - - if (channelInfo.ChannelName.Equals("G", StringComparison.Ordinal)) - { - hasGreenChannel = true; - } - - if (channelInfo.ChannelName.Equals("B", StringComparison.Ordinal)) - { - hasBlueChannel = true; - } - - if (channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal)) - { - hasLuminance = true; - } - } - - if (hasRedChannel && hasGreenChannel && hasBlueChannel && hasAlphaChannel) - { - this.ImageDataType = ExrImageDataType.Rgba; - return; - } - - if (hasRedChannel && hasGreenChannel && hasBlueChannel) - { - this.ImageDataType = ExrImageDataType.Rgb; - return; - } - - if (hasLuminance && this.Channels.Count == 1) - { - this.ImageDataType = ExrImageDataType.Gray; - return; - } - - ExrThrowHelper.ThrowNotSupported("The image contains channels, which are not supported!"); - } + ExrCompression.None or ExrCompression.Zip or ExrCompression.Zips or ExrCompression.RunLengthEncoded or ExrCompression.B44 => true, + _ => false, + }; private bool HasAlpha() { diff --git a/src/ImageSharp/Formats/Exr/ExrMetadata.cs b/src/ImageSharp/Formats/Exr/ExrMetadata.cs index 552d2d1133..32b9ef3c55 100644 --- a/src/ImageSharp/Formats/Exr/ExrMetadata.cs +++ b/src/ImageSharp/Formats/Exr/ExrMetadata.cs @@ -30,17 +30,22 @@ public ExrMetadata() /// public ExrPixelType PixelType { get; set; } = ExrPixelType.Float; + /// public static ExrMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException(); + /// public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel => throw new NotImplementedException(); /// public IDeepCloneable DeepClone() => new ExrMetadata(this); + /// public PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); + /// public FormatConnectingMetadata ToFormatConnectingMetadata() => throw new NotImplementedException(); + /// ExrMetadata IDeepCloneable.DeepClone() => throw new NotImplementedException(); } diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs index 34a7f67871..4913724473 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/Rgba32.cs @@ -318,6 +318,7 @@ public static Rgba32 FromRgba64(Rgba64 source) /// Initializes the pixel instance from an value. /// /// The value. + /// The pixel value as Rgba32. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Rgba32 FromRgb96(Rgb96 source) => new() diff --git a/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs b/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs new file mode 100644 index 0000000000..87d68f40f6 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; + +using ImageMagick; +using SixLabors.ImageSharp.Formats.Exr; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs; + +[MarkdownExporter] +[HtmlExporter] +[Config(typeof(Config.Short))] +public class DecodeExr +{ + private Configuration configuration; + + private byte[] imageBytes; + + private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); + + [Params(TestImages.Exr.Benchamrk)] + public string TestImage { get; set; } + + [GlobalSetup] + public void ReadImages() + { + this.configuration = Configuration.CreateDefaultInstance(); + new ExrConfigurationModule().Configure(this.configuration); + + this.imageBytes ??= File.ReadAllBytes(this.TestImageFullPath); + } + + [Benchmark(Description = "Magick Exr")] + public int ExrImageMagick() + { + MagickReadSettings settings = new() { Format = MagickFormat.Exr }; + using MemoryStream memoryStream = new(this.imageBytes); + using MagickImage image = new(memoryStream, settings); + return image.Width; + } + + [Benchmark(Description = "ImageSharp Exr")] + public int ExrImageSharp() + { + using MemoryStream memoryStream = new(this.imageBytes); + using Image image = Image.Load(memoryStream); + return image.Height; + } + + /* Results 27.03.2026 + BenchmarkDotNet v0.15.8, Windows 11 (10.0.26200.8037/25H2/2025Update/HudsonValley2) + Intel Core i7-14700T 1.30GHz, 1 CPU, 28 logical and 20 physical cores + .NET SDK 10.0.201 + [Host] : .NET 8.0.25 (8.0.25, 8.0.2526.11203), X64 RyuJIT x86-64-v3 + Job-VDWIGO : .NET 8.0.25 (8.0.25, 8.0.2526.11203), X64 RyuJIT x86-64-v3 + + Runtime=.NET 8.0 Arguments=/p:DebugType=portable IterationCount=3 + LaunchCount=1 WarmupCount=3 + + | Method | TestImage | Mean | Error | StdDev | Allocated | + |----------------- |----------------------------- |---------:|---------:|---------:|----------:| + | 'Magick Exr' | Exr/Calliphora_benchmark.exr | 20.37 ms | 0.790 ms | 0.043 ms | 12.98 KB | + | 'ImageSharp Exr' | Exr/Calliphora_benchmark.exr | 45.68 ms | 4.999 ms | 0.274 ms | 34.09 KB | + */ +} diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 1b0c0c865b..8758b74f5d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1383,6 +1383,7 @@ public static class Cur public static class Exr { + public const string Benchamrk = "Exr/Calliphora_benchmark.exr"; public const string Uncompressed = "Exr/Calliphora_uncompressed.exr"; public const string UncompressedFloatRgb = "Exr/rgb_float32_uncompressed.exr"; public const string UncompressedUintRgb = "Exr/rgb_uint32_uncompressed.exr"; diff --git a/tests/Images/Input/Exr/Calliphora_benchmark.exr b/tests/Images/Input/Exr/Calliphora_benchmark.exr new file mode 100644 index 0000000000..da416b3c82 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_benchmark.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3368860692927e709365f2d37b92411068e77a0f23624ff57af6089ec69f357 +size 2592888 From 9ead3ebe3aa88a5a23355c18f214de656d12deb1 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 27 Mar 2026 13:05:31 +0100 Subject: [PATCH 52/66] Update Magick.NET-Q16 to version 14.11.1 for fix to decoding zip compressed exr files --- tests/Directory.Build.targets | 2 +- .../ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs | 2 +- .../ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs | 2 +- .../Codecs/Webp/DecodeWebp.cs | 4 ++-- .../LoadResizeSave/LoadResizeSaveStressRunner.cs | 4 ++-- .../ReferenceCodecs/MagickReferenceDecoder.cs | 14 +++++++------- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index 8c88ff647d..6b25509ed8 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -24,7 +24,7 @@ Do not update to 14+ yet. There's differnce in how the BMP decoder handles rounding in 16 bit images. See https://github.com/ImageMagick/ImageMagick/commit/27a0a9c37f18af9c8d823a3ea076f600843b553c --> - + diff --git a/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs b/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs index 87d68f40f6..76cd9c0d61 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs @@ -34,7 +34,7 @@ public void ReadImages() } [Benchmark(Description = "Magick Exr")] - public int ExrImageMagick() + public uint ExrImageMagick() { MagickReadSettings settings = new() { Format = MagickFormat.Exr }; using MemoryStream memoryStream = new(this.imageBytes); diff --git a/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs b/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs index d6a6cf1fb4..4c81aee6d8 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Tga/DecodeTga.cs @@ -27,7 +27,7 @@ public void SetupData() => this.data = File.ReadAllBytes(this.TestImageFullPath); [Benchmark(Baseline = true, Description = "ImageMagick Tga")] - public int TgaImageMagick() + public uint TgaImageMagick() { MagickReadSettings settings = new() { Format = MagickFormat.Tga }; using MagickImage image = new(new MemoryStream(this.data), settings); diff --git a/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs b/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs index bba1bc1871..a10f1527f1 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Webp/DecodeWebp.cs @@ -42,7 +42,7 @@ public void ReadImages() } [Benchmark(Description = "Magick Lossy Webp")] - public int WebpLossyMagick() + public uint WebpLossyMagick() { MagickReadSettings settings = new() { Format = MagickFormat.WebP }; using MemoryStream memoryStream = new(this.webpLossyBytes); @@ -59,7 +59,7 @@ public int WebpLossy() } [Benchmark(Description = "Magick Lossless Webp")] - public int WebpLosslessMagick() + public uint WebpLosslessMagick() { MagickReadSettings settings = new() { Format = MagickFormat.WebP }; diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs index f8bf19d576..8835fdbcca 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs @@ -248,10 +248,10 @@ public async Task ImageSharpResizeAsync(string input) public void MagickResize(string input) { using MagickImage image = new(input); - this.LogImageProcessed(image.Width, image.Height); + this.LogImageProcessed((int)image.Width, (int)image.Height); // Resize it to fit a 150x150 square - image.Resize(this.ThumbnailSize, this.ThumbnailSize); + image.Resize((uint)this.ThumbnailSize, (uint)this.ThumbnailSize); // Reduce the size of the file image.Strip(); diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index a63818562f..d72556b0e3 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -61,14 +61,14 @@ protected override Image Decode(DecoderOptions options, Stream s MagickReadSettings settings = new() { - FrameCount = (int)options.MaxFrames + FrameCount = options.MaxFrames }; settings.SetDefines(bmpReadDefines); settings.SetDefines(pngReadDefines); using MagickImageCollection magickImageCollection = new(stream, settings); - int imageWidth = magickImageCollection.Max(x => x.Width); - int imageHeight = magickImageCollection.Max(x => x.Height); + int imageWidth = (int)magickImageCollection.Max(x => x.Width); + int imageHeight = (int)magickImageCollection.Max(x => x.Height); List> framesList = []; foreach (IMagickImage magicFrame in magickImageCollection) @@ -77,10 +77,10 @@ protected override Image Decode(DecoderOptions options, Stream s framesList.Add(frame); Buffer2DRegion buffer = frame.PixelBuffer.GetRegion( - imageWidth - magicFrame.Width, - imageHeight - magicFrame.Height, - magicFrame.Width, - magicFrame.Height); + (int)(imageWidth - magicFrame.Width), + (int)(imageHeight - magicFrame.Height), + (int)magicFrame.Width, + (int)magicFrame.Height); using IUnsafePixelCollection pixels = magicFrame.GetPixelsUnsafe(); if (magicFrame.Depth is 12 or 10 or 8 or 6 or 5 or 4 or 3 or 2 or 1) From aa0af92faaf7bb5ad4b3365709271ffa9c51bb1c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 27 Mar 2026 16:45:01 +0100 Subject: [PATCH 53/66] Use CompareToReferenceOutput for int pixel type --- tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs | 4 +++- ...d_Rgb_ExrPixelType_Uint_Rgba32_rgb_uint32_uncompressed.png | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint_Rgba32_rgb_uint32_uncompressed.png diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index beca934484..57abd0147d 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -52,7 +52,9 @@ public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint(Test { using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, ReferenceDecoder); + + // Compare to referene output, since the reference decoder does not support this pixel type. + image.CompareToReferenceOutput(provider); } [Theory] diff --git a/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint_Rgba32_rgb_uint32_uncompressed.png b/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint_Rgba32_rgb_uint32_uncompressed.png new file mode 100644 index 0000000000..d27a842a07 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint_Rgba32_rgb_uint32_uncompressed.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15adaff9755ff38f0a2b30180ced9e52ac1bf51b8f3b80fd26d85f18c0eef686 +size 68918 From 53230af86bf5bf05e9a3d46cce6b704725823d2f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 27 Mar 2026 17:20:43 +0100 Subject: [PATCH 54/66] Use tolerant comparer for B44 compression --- tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index 57abd0147d..fca8b63670 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -3,6 +3,7 @@ using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; namespace SixLabors.ImageSharp.Tests.Formats.Exr; @@ -42,7 +43,9 @@ public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Float(Tes { using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, ReferenceDecoder); + + // There is a 0,0059% difference to the Reference decoder. + image.CompareToOriginal(provider, ImageComparer.Tolerant(0.0005f), ReferenceDecoder); } [Theory] @@ -114,6 +117,8 @@ public void ExrDecoder_CanDecode_B44Compressed(TestImageProvider { using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, ReferenceDecoder); + + // Note: There is a 0,1190% difference to the reference decoder. + image.CompareToOriginal(provider, ImageComparer.Tolerant(0.011f), ReferenceDecoder); } } From a741e84a127dff39cc424f75afa1961a04176bfb Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 27 Mar 2026 17:23:17 +0100 Subject: [PATCH 55/66] Use exact comparer as default for exr decoder tests --- .../ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index fca8b63670..34cad442f1 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -33,7 +33,7 @@ public void ExrDecoder_CanDecode_Uncompressed_Rgba_ExrPixelType_Half(Tes { using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, ReferenceDecoder); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); } [Theory] @@ -67,7 +67,7 @@ public void ExrDecoder_CanDecode_Rgb(TestImageProvider provider) { using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, ReferenceDecoder); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); } [Theory] @@ -77,7 +77,7 @@ public void ExrDecoder_CanDecode_Gray(TestImageProvider provider { using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, ReferenceDecoder); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); } [Theory] @@ -87,7 +87,7 @@ public void ExrDecoder_CanDecode_ZipCompressed(TestImageProvider { using Image image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, ReferenceDecoder); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); } [Theory] @@ -97,7 +97,7 @@ public void ExrDecoder_CanDecode_ZipsCompressed(TestImageProvider image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, ReferenceDecoder); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); } [Theory] @@ -107,7 +107,7 @@ public void ExrDecoder_CanDecode_RunLengthCompressed(TestImageProvider image = provider.GetImage(ExrDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider, ReferenceDecoder); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); } [Theory] From 8c0c85442cec2a1753335e897151109b625b02be Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 27 Mar 2026 19:20:53 +0100 Subject: [PATCH 56/66] Set exr pixel type when decoding an image --- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 18 +++++++++++++--- src/ImageSharp/Formats/Exr/ExrMetadata.cs | 14 ++++++------- .../Formats/Exr/ExrDecoderTests.cs | 21 +++++++++++++++++++ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 4f3f113536..9cff42a4e0 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -43,6 +43,11 @@ internal sealed class ExrDecoderCore : ImageDecoderCore /// private ImageMetadata metadata; + /// + /// The exr specific metadata. + /// + private ExrMetadata exrMetadata; + /// /// Initializes a new instance of the class. /// @@ -74,6 +79,11 @@ public ExrDecoderCore(ExrDecoderOptions options) /// private ExrCompression Compression { get; set; } + /// + /// Gets or sets the pixel type. + /// + private ExrPixelType PixelType { get; set; } + /// /// Gets or sets the header attributes. /// @@ -88,12 +98,10 @@ protected override Image Decode(BufferedReadStream stream, Cance ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported"); } - ExrPixelType pixelType = this.ValidateChannels(); - Image image = new(this.configuration, this.Width, this.Height, this.metadata); Buffer2D pixels = image.GetRootFramePixelBuffer(); - switch (pixelType) + switch (this.PixelType) { case ExrPixelType.Half: case ExrPixelType.Float: @@ -422,9 +430,13 @@ private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) this.Height = this.HeaderAttributes.DataWindow.YMax - this.HeaderAttributes.DataWindow.YMin + 1; this.Channels = this.HeaderAttributes.Channels; this.Compression = this.HeaderAttributes.Compression; + this.PixelType = this.ValidateChannels(); this.metadata = new ImageMetadata(); + this.exrMetadata = this.metadata.GetExrMetadata(); + this.exrMetadata.PixelType = this.PixelType; + return this.HeaderAttributes; } diff --git a/src/ImageSharp/Formats/Exr/ExrMetadata.cs b/src/ImageSharp/Formats/Exr/ExrMetadata.cs index 32b9ef3c55..b32da8f038 100644 --- a/src/ImageSharp/Formats/Exr/ExrMetadata.cs +++ b/src/ImageSharp/Formats/Exr/ExrMetadata.cs @@ -28,24 +28,24 @@ public ExrMetadata() /// /// Gets or sets the pixel format. /// - public ExrPixelType PixelType { get; set; } = ExrPixelType.Float; - - /// - public static ExrMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException(); + public ExrPixelType PixelType { get; set; } = ExrPixelType.Half; /// public void AfterImageApply(Image destination, Matrix4x4 matrix) where TPixel : unmanaged, IPixel => throw new NotImplementedException(); - /// - public IDeepCloneable DeepClone() => new ExrMetadata(this); - /// public PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); /// public FormatConnectingMetadata ToFormatConnectingMetadata() => throw new NotImplementedException(); + /// + public static ExrMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException(); + /// ExrMetadata IDeepCloneable.DeepClone() => throw new NotImplementedException(); + + /// + public IDeepCloneable DeepClone() => new ExrMetadata(this); } diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index 34cad442f1..0077523ad2 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Exr; +using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; @@ -21,19 +22,35 @@ public void ExrDecoder_Identify_DetectsCorrectWidthAndHeight(string imag TestFile testFile = TestFile.Create(imagePath); using MemoryStream stream = new(testFile.Bytes, false); ImageInfo imageInfo = Image.Identify(stream); + Assert.NotNull(imageInfo); Assert.Equal(expectedWidth, imageInfo.Width); Assert.Equal(expectedHeight, imageInfo.Height); } + [Theory] + [InlineData(TestImages.Exr.Uncompressed)] + public void ExrDecoder_Identify_DetectsCorrectPixelType(string imagePath) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + ExrMetadata exrMetaData = imageInfo.Metadata.GetExrMetadata(); + + Assert.NotNull(imageInfo); + Assert.Equal(ExrPixelType.Float, exrMetaData.PixelType); + } + [Theory] [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] public void ExrDecoder_CanDecode_Uncompressed_Rgba_ExrPixelType_Half(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(ExrDecoder.Instance); + ExrMetadata exrMetaData = image.Metadata.GetExrMetadata(); image.DebugSave(provider); image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); + Assert.Equal(ExrPixelType.Half, exrMetaData.PixelType); } [Theory] @@ -42,10 +59,12 @@ public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Float(Tes where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(ExrDecoder.Instance); + ExrMetadata exrMetaData = image.Metadata.GetExrMetadata(); image.DebugSave(provider); // There is a 0,0059% difference to the Reference decoder. image.CompareToOriginal(provider, ImageComparer.Tolerant(0.0005f), ReferenceDecoder); + Assert.Equal(ExrPixelType.Float, exrMetaData.PixelType); } [Theory] @@ -54,10 +73,12 @@ public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Uint(Test where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(ExrDecoder.Instance); + ExrMetadata exrMetaData = image.Metadata.GetExrMetadata(); image.DebugSave(provider); // Compare to referene output, since the reference decoder does not support this pixel type. image.CompareToReferenceOutput(provider); + Assert.Equal(ExrPixelType.UnsignedInt, exrMetaData.PixelType); } [Theory] From c8c3656aaf767c6977cdd598738f2d3c418bcd41 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 28 Mar 2026 18:18:42 +0100 Subject: [PATCH 57/66] Implement GetPixelTypeInfo() for EXR --- .../Formats/Exr/Constants/ExrImageDataType.cs | 17 ++++- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 60 ++++++++++++++++++ src/ImageSharp/Formats/Exr/ExrMetadata.cs | 63 ++++++++++++++++++- .../Formats/Exr/ExrDecoderTests.cs | 28 ++++++++- 4 files changed, 163 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs b/src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs index d63a8e2198..9453a7d9cd 100644 --- a/src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs +++ b/src/ImageSharp/Formats/Exr/Constants/ExrImageDataType.cs @@ -3,13 +3,28 @@ namespace SixLabors.ImageSharp.Formats.Exr.Constants; -internal enum ExrImageDataType +/// +/// This enum represents the type of pixel data in the EXR image. +/// +public enum ExrImageDataType { + /// + /// The pixel data is unknown. + /// Unknown = 0, + /// + /// The pixel data has 3 channels: red, green and blue. + /// Rgb = 1, + /// + /// The pixel data has four channels: red, green, blue and a alpha channel. + /// Rgba = 2, + /// + /// There is only one channel with the luminance. + /// Gray = 3, } diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 9cff42a4e0..1127b146c5 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -79,6 +79,11 @@ public ExrDecoderCore(ExrDecoderOptions options) /// private ExrCompression Compression { get; set; } + /// + /// Gets or sets the image data type, either RGB, RGBA or gray. + /// + private ExrImageDataType ImageDataType { get; set; } + /// /// Gets or sets the pixel type. /// @@ -403,6 +408,59 @@ private ExrPixelType ValidateChannels() return pixelType; } + private ExrImageDataType ReadImageDataType() + { + bool hasRedChannel = false; + bool hasGreenChannel = false; + bool hasBlueChannel = false; + bool hasAlphaChannel = false; + bool hasLuminance = false; + foreach (ExrChannelInfo channelInfo in this.Channels) + { + if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal)) + { + hasAlphaChannel = true; + } + + if (channelInfo.ChannelName.Equals("R", StringComparison.Ordinal)) + { + hasRedChannel = true; + } + + if (channelInfo.ChannelName.Equals("G", StringComparison.Ordinal)) + { + hasGreenChannel = true; + } + + if (channelInfo.ChannelName.Equals("B", StringComparison.Ordinal)) + { + hasBlueChannel = true; + } + + if (channelInfo.ChannelName.Equals("Y", StringComparison.Ordinal)) + { + hasLuminance = true; + } + } + + if (hasRedChannel && hasGreenChannel && hasBlueChannel && hasAlphaChannel) + { + return ExrImageDataType.Rgba; + } + + if (hasRedChannel && hasGreenChannel && hasBlueChannel) + { + return ExrImageDataType.Rgb; + } + + if (hasLuminance && this.Channels.Count == 1) + { + return ExrImageDataType.Gray; + } + + return ExrImageDataType.Unknown; + } + private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) { // Skip over the magick bytes, we already know its an EXR image. @@ -431,11 +489,13 @@ private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) this.Channels = this.HeaderAttributes.Channels; this.Compression = this.HeaderAttributes.Compression; this.PixelType = this.ValidateChannels(); + this.ImageDataType = this.ReadImageDataType(); this.metadata = new ImageMetadata(); this.exrMetadata = this.metadata.GetExrMetadata(); this.exrMetadata.PixelType = this.PixelType; + this.exrMetadata.ImageDataType = this.ImageDataType; return this.HeaderAttributes; } diff --git a/src/ImageSharp/Formats/Exr/ExrMetadata.cs b/src/ImageSharp/Formats/Exr/ExrMetadata.cs index b32da8f038..7abba6da89 100644 --- a/src/ImageSharp/Formats/Exr/ExrMetadata.cs +++ b/src/ImageSharp/Formats/Exr/ExrMetadata.cs @@ -30,12 +30,69 @@ public ExrMetadata() /// public ExrPixelType PixelType { get; set; } = ExrPixelType.Half; + /// + /// Gets or sets the image data type, either RGB, RGBA or gray. + /// + public ExrImageDataType ImageDataType { get; set; } = ExrImageDataType.Unknown; + /// - public void AfterImageApply(Image destination, Matrix4x4 matrix) - where TPixel : unmanaged, IPixel => throw new NotImplementedException(); + public PixelTypeInfo GetPixelTypeInfo() + { + bool hasAlpha = this.ImageDataType is ExrImageDataType.Rgba; + + int bitsPerComponent = 32; + int bitsPerPixel = hasAlpha ? bitsPerComponent * 4 : bitsPerComponent * 3; + if (this.PixelType == ExrPixelType.Half) + { + bitsPerComponent = 16; + bitsPerPixel = hasAlpha ? bitsPerComponent * 4 : bitsPerComponent * 3; + } + + PixelAlphaRepresentation alpha = hasAlpha ? PixelAlphaRepresentation.Unassociated : PixelAlphaRepresentation.None; + PixelColorType color = PixelColorType.RGB; + + int componentsCount = 0; + int[] precision = []; + switch (this.ImageDataType) + { + case ExrImageDataType.Rgb: + color = PixelColorType.RGB; + componentsCount = 3; + precision = new int[componentsCount]; + precision[0] = bitsPerComponent; + precision[1] = bitsPerComponent; + precision[2] = bitsPerComponent; + break; + case ExrImageDataType.Rgba: + color = PixelColorType.RGB | PixelColorType.Alpha; + componentsCount = 4; + precision = new int[componentsCount]; + precision[0] = bitsPerComponent; + precision[1] = bitsPerComponent; + precision[2] = bitsPerComponent; + precision[3] = bitsPerComponent; + break; + case ExrImageDataType.Gray: + color = PixelColorType.Luminance; + componentsCount = 1; + precision = new int[componentsCount]; + precision[0] = bitsPerComponent; + break; + } + + PixelComponentInfo info = PixelComponentInfo.Create(componentsCount, bitsPerPixel, precision); + return new PixelTypeInfo(bitsPerPixel) + { + AlphaRepresentation = alpha, + ComponentInfo = info, + ColorType = color + }; + } + /// - public PixelTypeInfo GetPixelTypeInfo() => throw new NotImplementedException(); + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel => throw new NotImplementedException(); /// public FormatConnectingMetadata ToFormatConnectingMetadata() => throw new NotImplementedException(); diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index 0077523ad2..0d6ea213a6 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -30,7 +30,20 @@ public void ExrDecoder_Identify_DetectsCorrectWidthAndHeight(string imag [Theory] [InlineData(TestImages.Exr.Uncompressed)] - public void ExrDecoder_Identify_DetectsCorrectPixelType(string imagePath) + public void ExrDecoder_Identify_DetectsCorrectPixelType_Half(string imagePath) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + ExrMetadata exrMetaData = imageInfo.Metadata.GetExrMetadata(); + + Assert.NotNull(imageInfo); + Assert.Equal(ExrPixelType.Half, exrMetaData.PixelType); + } + + [Theory] + [InlineData(TestImages.Exr.UncompressedFloatRgb)] + public void ExrDecoder_Identify_DetectsCorrectPixelType_Float(string imagePath) { TestFile testFile = TestFile.Create(imagePath); using MemoryStream stream = new(testFile.Bytes, false); @@ -41,6 +54,19 @@ public void ExrDecoder_Identify_DetectsCorrectPixelType(string imagePath Assert.Equal(ExrPixelType.Float, exrMetaData.PixelType); } + [Theory] + [InlineData(TestImages.Exr.UncompressedUintRgb)] + public void ExrDecoder_Identify_DetectsCorrectPixelType_Int(string imagePath) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + ExrMetadata exrMetaData = imageInfo.Metadata.GetExrMetadata(); + + Assert.NotNull(imageInfo); + Assert.Equal(ExrPixelType.UnsignedInt, exrMetaData.PixelType); + } + [Theory] [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] public void ExrDecoder_CanDecode_Uncompressed_Rgba_ExrPixelType_Half(TestImageProvider provider) From b95663489eefebb91e7d91a1bfcb2c8c2b6ec8dc Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 29 Mar 2026 16:49:53 +0200 Subject: [PATCH 58/66] Add tests for exr metadata --- src/ImageSharp/Formats/Exr/ExrMetadata.cs | 19 +++-- .../Codecs/Exr/DecodeExr.cs | 2 +- .../Formats/Exr/ExrDecoderTests.cs | 54 +------------- .../Formats/Exr/ExrMetadataTests.cs | 70 +++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 3 +- .../Exr/Calliphora_uncompressed_rgba.exr | 3 + 6 files changed, 89 insertions(+), 62 deletions(-) create mode 100644 tests/ImageSharp.Tests/Formats/Exr/ExrMetadataTests.cs create mode 100644 tests/Images/Input/Exr/Calliphora_uncompressed_rgba.exr diff --git a/src/ImageSharp/Formats/Exr/ExrMetadata.cs b/src/ImageSharp/Formats/Exr/ExrMetadata.cs index 7abba6da89..f618b6d6f9 100644 --- a/src/ImageSharp/Formats/Exr/ExrMetadata.cs +++ b/src/ImageSharp/Formats/Exr/ExrMetadata.cs @@ -89,20 +89,25 @@ public PixelTypeInfo GetPixelTypeInfo() }; } - /// - public void AfterImageApply(Image destination, Matrix4x4 matrix) - where TPixel : unmanaged, IPixel => throw new NotImplementedException(); + public FormatConnectingMetadata ToFormatConnectingMetadata() => new() + { + EncodingType = EncodingType.Lossless, + PixelTypeInfo = this.GetPixelTypeInfo() + }; /// - public FormatConnectingMetadata ToFormatConnectingMetadata() => throw new NotImplementedException(); + public static ExrMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => new() { PixelType = ExrPixelType.Half }; /// - public static ExrMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) => throw new NotImplementedException(); + ExrMetadata IDeepCloneable.DeepClone() => new(this); /// - ExrMetadata IDeepCloneable.DeepClone() => throw new NotImplementedException(); + public IDeepCloneable DeepClone() => new ExrMetadata(this); /// - public IDeepCloneable DeepClone() => new ExrMetadata(this); + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + } } diff --git a/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs b/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs index 76cd9c0d61..45021cadc0 100644 --- a/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs +++ b/tests/ImageSharp.Benchmarks/Codecs/Exr/DecodeExr.cs @@ -21,7 +21,7 @@ public class DecodeExr private string TestImageFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, this.TestImage); - [Params(TestImages.Exr.Benchamrk)] + [Params(TestImages.Exr.Benchmark)] public string TestImage { get; set; } [GlobalSetup] diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index 0d6ea213a6..e6a00c84d5 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -15,61 +15,9 @@ public class ExrDecoderTests { private static MagickReferenceDecoder ReferenceDecoder => MagickReferenceDecoder.Exr; - [Theory] - [InlineData(TestImages.Exr.Uncompressed, 199, 297)] - public void ExrDecoder_Identify_DetectsCorrectWidthAndHeight(string imagePath, int expectedWidth, int expectedHeight) - { - TestFile testFile = TestFile.Create(imagePath); - using MemoryStream stream = new(testFile.Bytes, false); - ImageInfo imageInfo = Image.Identify(stream); - - Assert.NotNull(imageInfo); - Assert.Equal(expectedWidth, imageInfo.Width); - Assert.Equal(expectedHeight, imageInfo.Height); - } - - [Theory] - [InlineData(TestImages.Exr.Uncompressed)] - public void ExrDecoder_Identify_DetectsCorrectPixelType_Half(string imagePath) - { - TestFile testFile = TestFile.Create(imagePath); - using MemoryStream stream = new(testFile.Bytes, false); - ImageInfo imageInfo = Image.Identify(stream); - ExrMetadata exrMetaData = imageInfo.Metadata.GetExrMetadata(); - - Assert.NotNull(imageInfo); - Assert.Equal(ExrPixelType.Half, exrMetaData.PixelType); - } - - [Theory] - [InlineData(TestImages.Exr.UncompressedFloatRgb)] - public void ExrDecoder_Identify_DetectsCorrectPixelType_Float(string imagePath) - { - TestFile testFile = TestFile.Create(imagePath); - using MemoryStream stream = new(testFile.Bytes, false); - ImageInfo imageInfo = Image.Identify(stream); - ExrMetadata exrMetaData = imageInfo.Metadata.GetExrMetadata(); - - Assert.NotNull(imageInfo); - Assert.Equal(ExrPixelType.Float, exrMetaData.PixelType); - } - - [Theory] - [InlineData(TestImages.Exr.UncompressedUintRgb)] - public void ExrDecoder_Identify_DetectsCorrectPixelType_Int(string imagePath) - { - TestFile testFile = TestFile.Create(imagePath); - using MemoryStream stream = new(testFile.Bytes, false); - ImageInfo imageInfo = Image.Identify(stream); - ExrMetadata exrMetaData = imageInfo.Metadata.GetExrMetadata(); - - Assert.NotNull(imageInfo); - Assert.Equal(ExrPixelType.UnsignedInt, exrMetaData.PixelType); - } - [Theory] [WithFile(TestImages.Exr.Uncompressed, PixelTypes.Rgba32)] - public void ExrDecoder_CanDecode_Uncompressed_Rgba_ExrPixelType_Half(TestImageProvider provider) + public void ExrDecoder_CanDecode_Uncompressed_Rgb_ExrPixelType_Half(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(ExrDecoder.Instance); diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrMetadataTests.cs new file mode 100644 index 0000000000..c94b6dce54 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrMetadataTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Exr; +using SixLabors.ImageSharp.Formats.Exr.Constants; + +namespace SixLabors.ImageSharp.Tests.Formats.Exr; + +[Trait("Format", "Exr")] +public class ExrMetadataTests +{ + [Fact] + public void CloneIsDeep() + { + ExrMetadata meta = new() + { ImageDataType = ExrImageDataType.Rgb, PixelType = ExrPixelType.Half }; + ExrMetadata clone = (ExrMetadata)meta.DeepClone(); + + clone.ImageDataType = ExrImageDataType.Gray; + clone.PixelType = ExrPixelType.Float; + + Assert.False(meta.ImageDataType.Equals(clone.ImageDataType)); + Assert.False(meta.PixelType.Equals(clone.PixelType)); + } + + [Theory] + [InlineData(TestImages.Exr.Uncompressed, 199, 297)] + public void Identify_DetectsCorrectWidthAndHeight(string imagePath, int expectedWidth, int expectedHeight) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + Assert.Equal(expectedWidth, imageInfo.Width); + Assert.Equal(expectedHeight, imageInfo.Height); + } + + [Theory] + [InlineData(TestImages.Exr.Uncompressed, ExrPixelType.Half)] + [InlineData(TestImages.Exr.UncompressedFloatRgb, ExrPixelType.Float)] + [InlineData(TestImages.Exr.UncompressedUintRgb, ExrPixelType.UnsignedInt)] + public void Identify_DetectsCorrectPixelType(string imagePath, ExrPixelType expectedPixelType) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + ExrMetadata metadata = imageInfo.Metadata.GetExrMetadata(); + Assert.NotNull(metadata); + Assert.Equal(expectedPixelType, metadata.PixelType); + } + + [Theory] + [InlineData(TestImages.Exr.UncompressedRgba, ExrImageDataType.Rgba)] + [InlineData(TestImages.Exr.Rgb, ExrImageDataType.Rgb)] + [InlineData(TestImages.Exr.Gray, ExrImageDataType.Gray)] + public void Identify_DetectsCorrectImageDataType(string imagePath, ExrImageDataType expectedImageDataType) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + ExrMetadata metadata = imageInfo.Metadata.GetExrMetadata(); + Assert.NotNull(metadata); + Assert.Equal(expectedImageDataType, metadata.ImageDataType); + } +} diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 8758b74f5d..7cdc50d1ee 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1383,8 +1383,9 @@ public static class Cur public static class Exr { - public const string Benchamrk = "Exr/Calliphora_benchmark.exr"; + public const string Benchmark = "Exr/Calliphora_benchmark.exr"; public const string Uncompressed = "Exr/Calliphora_uncompressed.exr"; + public const string UncompressedRgba = "Exr/Calliphora_uncompressed_rgba.exr"; public const string UncompressedFloatRgb = "Exr/rgb_float32_uncompressed.exr"; public const string UncompressedUintRgb = "Exr/rgb_uint32_uncompressed.exr"; public const string Zip = "Exr/Calliphora_zip.exr"; diff --git a/tests/Images/Input/Exr/Calliphora_uncompressed_rgba.exr b/tests/Images/Input/Exr/Calliphora_uncompressed_rgba.exr new file mode 100644 index 0000000000..86bc1d354a --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_uncompressed_rgba.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e9f6b5afa3c10c895ba67b51568295be40c2b1057224437600028487c581291 +size 481899 From a9f6dbc68b8f09d57b5ac2d9fa08f430a322db2f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 29 Mar 2026 17:19:46 +0200 Subject: [PATCH 59/66] Add compression to exr metadata --- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 1 + src/ImageSharp/Formats/Exr/ExrMetadata.cs | 7 ++++++- .../Formats/Exr/ExrMetadataTests.cs | 20 ++++++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 1127b146c5..8a893c3c83 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -496,6 +496,7 @@ private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) this.exrMetadata = this.metadata.GetExrMetadata(); this.exrMetadata.PixelType = this.PixelType; this.exrMetadata.ImageDataType = this.ImageDataType; + this.exrMetadata.Compression = this.Compression; return this.HeaderAttributes; } diff --git a/src/ImageSharp/Formats/Exr/ExrMetadata.cs b/src/ImageSharp/Formats/Exr/ExrMetadata.cs index f618b6d6f9..1fa724657f 100644 --- a/src/ImageSharp/Formats/Exr/ExrMetadata.cs +++ b/src/ImageSharp/Formats/Exr/ExrMetadata.cs @@ -35,6 +35,11 @@ public ExrMetadata() /// public ExrImageDataType ImageDataType { get; set; } = ExrImageDataType.Unknown; + /// + /// Gets or sets the compression method. + /// + public ExrCompression Compression { get; set; } = ExrCompression.None; + /// public PixelTypeInfo GetPixelTypeInfo() { @@ -92,7 +97,7 @@ public PixelTypeInfo GetPixelTypeInfo() /// public FormatConnectingMetadata ToFormatConnectingMetadata() => new() { - EncodingType = EncodingType.Lossless, + EncodingType = this.Compression is ExrCompression.B44 or ExrCompression.B44A or ExrCompression.Pxr24 ? EncodingType.Lossy : EncodingType.Lossless, PixelTypeInfo = this.GetPixelTypeInfo() }; diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrMetadataTests.cs index c94b6dce54..2cbbf0ec31 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrMetadataTests.cs @@ -13,14 +13,16 @@ public class ExrMetadataTests public void CloneIsDeep() { ExrMetadata meta = new() - { ImageDataType = ExrImageDataType.Rgb, PixelType = ExrPixelType.Half }; + { ImageDataType = ExrImageDataType.Rgb, PixelType = ExrPixelType.Half, Compression = ExrCompression.None }; ExrMetadata clone = (ExrMetadata)meta.DeepClone(); clone.ImageDataType = ExrImageDataType.Gray; clone.PixelType = ExrPixelType.Float; + clone.Compression = ExrCompression.Zip; Assert.False(meta.ImageDataType.Equals(clone.ImageDataType)); Assert.False(meta.PixelType.Equals(clone.PixelType)); + Assert.False(meta.Compression.Equals(clone.Compression)); } [Theory] @@ -67,4 +69,20 @@ public void Identify_DetectsCorrectImageDataType(string imagePath, ExrImageDataT Assert.NotNull(metadata); Assert.Equal(expectedImageDataType, metadata.ImageDataType); } + + [Theory] + [InlineData(TestImages.Exr.UncompressedRgba, ExrCompression.None)] + [InlineData(TestImages.Exr.B44, ExrCompression.B44)] + [InlineData(TestImages.Exr.Rle, ExrCompression.RunLengthEncoded)] + public void Identify_DetectsCorrectCompression(string imagePath, ExrCompression expectedCompression) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + ImageInfo imageInfo = Image.Identify(stream); + + Assert.NotNull(imageInfo); + ExrMetadata metadata = imageInfo.Metadata.GetExrMetadata(); + Assert.NotNull(metadata); + Assert.Equal(expectedCompression, metadata.Compression); + } } From 7939cc9514935239f9bac429a7770154673dd60f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 29 Mar 2026 19:31:33 +0200 Subject: [PATCH 60/66] Throw InvalidImageContentException when requiered header fields are missing --- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 8a893c3c83..126c4c8f69 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -562,6 +562,41 @@ private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) attribute = this.ReadAttribute(stream); } + if (!displayWindow.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the displayWindow attribute is missing!"); + } + + if (!dataWindow.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the dataWindow attribute is missing!"); + } + + if (channels is null) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the channels attribute is missing!"); + } + + if (!compression.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the compression attribute is missing!"); + } + + if (!lineOrder.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the lineOrder attribute is missing!"); + } + + if (!screenWindowWidth.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the screenWindowWidth attribute is missing!"); + } + + if (!screenWindowCenterX.HasValue || !screenWindowCenterY.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the screenWindowCenter attribute is missing!"); + } + ExrHeaderAttributes header = new( channels, compression.Value, From a1152c5dc3ccfe73ea18b40e5344e76ebb60e68b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 29 Mar 2026 19:57:03 +0200 Subject: [PATCH 61/66] Add some more comments --- .../Formats/Exr/ExrHeaderAttributes.cs | 37 +++++++++++++++++++ src/ImageSharp/Formats/Exr/ExrUtils.cs | 11 ++++++ .../Formats/Exr/IExrEncoderOptions.cs | 17 --------- 3 files changed, 48 insertions(+), 17 deletions(-) delete mode 100644 src/ImageSharp/Formats/Exr/IExrEncoderOptions.cs diff --git a/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs b/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs index afda62bf92..cdcddd1175 100644 --- a/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs +++ b/src/ImageSharp/Formats/Exr/ExrHeaderAttributes.cs @@ -5,6 +5,10 @@ namespace SixLabors.ImageSharp.Formats.Exr; +/// +/// The header of an EXR image. +/// +/// internal class ExrHeaderAttributes { public ExrHeaderAttributes( @@ -33,25 +37,58 @@ public ExrHeaderAttributes( this.ChunkCount = chunkCount; } + /// + /// Gets or sets a description of the image channels stored in the file. + /// public IList Channels { get; set; } + /// + /// Gets or sets the compression method applied to the pixel data of all channels in the file. + /// public ExrCompression Compression { get; set; } + /// + /// Gets or sets the image’s data window. + /// public ExrBox2i DataWindow { get; set; } + /// + /// Gets or sets the image’s display window. + /// public ExrBox2i DisplayWindow { get; set; } + /// + /// Gets or sets in what order the scan lines in the file are stored in the file (increasing Y, decreasing Y, or, for tiled images, also random Y). + /// public ExrLineOrder LineOrder { get; set; } + /// + /// Gets or sets the aspect ratio of the image. + /// public float AspectRatio { get; set; } + /// + /// Gets or sets the screen width. + /// public float ScreenWindowWidth { get; set; } + /// + /// Gets or sets the screen window center. + /// public PointF ScreenWindowCenter { get; set; } + /// + /// Gets or sets the number of horizontal tiles. + /// public uint? TileXSize { get; set; } + /// + /// Gets or sets the number of vertical tiles. + /// public uint? TileYSize { get; set; } + /// + /// Gets or sets the chunk count. Indicates the number of chunks in this part. Required if the multipart bit (12) is set. + /// public int? ChunkCount { get; set; } } diff --git a/src/ImageSharp/Formats/Exr/ExrUtils.cs b/src/ImageSharp/Formats/Exr/ExrUtils.cs index 4d172c6ef8..386210b81d 100644 --- a/src/ImageSharp/Formats/Exr/ExrUtils.cs +++ b/src/ImageSharp/Formats/Exr/ExrUtils.cs @@ -7,6 +7,12 @@ namespace SixLabors.ImageSharp.Formats.Exr; internal static class ExrUtils { + /// + /// Calcualtes the required bytes for a pixel row. + /// + /// The image channels array. + /// The width in pixels of a row. + /// The number of bytes per row. public static uint CalculateBytesPerRow(IList channels, uint width) { uint bytesPerRow = 0; @@ -32,6 +38,11 @@ public static uint CalculateBytesPerRow(IList channels, uint wid return bytesPerRow; } + /// + /// Determines how many pixel rows there are in a block. This varies depending on the compression used. + /// + /// The compression used. + /// Pixel rows in a block. public static uint RowsPerBlock(ExrCompression compression) => compression switch { ExrCompression.Zip or ExrCompression.Pxr24 => 16, diff --git a/src/ImageSharp/Formats/Exr/IExrEncoderOptions.cs b/src/ImageSharp/Formats/Exr/IExrEncoderOptions.cs deleted file mode 100644 index 2ccf26ec17..0000000000 --- a/src/ImageSharp/Formats/Exr/IExrEncoderOptions.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.ImageSharp.Formats.Exr.Constants; - -namespace SixLabors.ImageSharp.Formats.Exr; - -/// -/// Configuration options for use during OpenExr encoding. -/// -internal interface IExrEncoderOptions -{ - /// - /// Gets the pixel type of the image. - /// - ExrPixelType? PixelType { get; } -} From 0c3ca3da2487527b9667eb24edb5431ae7902bab Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 30 Mar 2026 10:39:32 +0200 Subject: [PATCH 62/66] Fix failing bmp tests --- src/ImageSharp/Formats/Exr/ExrBox2i.cs | 2 +- .../Formats/Bmp/BmpDecoderTests.cs | 35 +++++++++++++++++-- ...erted_Rgba32_RunLengthEncoded-inverted.png | 3 ++ ...EOL_MagickRefDecoder_Rgba32_pal8rlecut.png | 3 ++ 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_Inverted_Rgba32_RunLengthEncoded-inverted.png create mode 100644 tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDeltaAndEOL_MagickRefDecoder_Rgba32_pal8rlecut.png diff --git a/src/ImageSharp/Formats/Exr/ExrBox2i.cs b/src/ImageSharp/Formats/Exr/ExrBox2i.cs index 61da9b36dc..032e60d929 100644 --- a/src/ImageSharp/Formats/Exr/ExrBox2i.cs +++ b/src/ImageSharp/Formats/Exr/ExrBox2i.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Formats.Exr; [DebuggerDisplay("xMin: {XMin}, yMin: {YMin}, xMax: {XMax}, yMax: {YMax}")] -internal struct ExrBox2i +internal readonly struct ExrBox2i { public ExrBox2i(int xMin, int yMin, int xMax, int yMax) { diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index caa6c507dc..3b9ab22ad4 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -224,8 +224,8 @@ public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDelta_SystemDrawingRe } } + // An RLE-compressed image that uses “delta” codes, to skip over some pixels. [Theory] - [WithFile(RLE8Cut, PixelTypes.Rgba32)] [WithFile(RLE8Delta, PixelTypes.Rgba32)] public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDelta_MagickRefDecoder(TestImageProvider provider) where TPixel : unmanaged, IPixel @@ -236,11 +236,21 @@ public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDelta_MagickRefDecode image.CompareToOriginal(provider, MagickReferenceDecoder.Png); } + // An RLE-compressed image that uses “delta” codes, and early EOL & EOBMP markers, to skip over some pixels. + [Theory] + [WithFile(RLE8Cut, PixelTypes.Rgba32)] + public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDeltaAndEOL_MagickRefDecoder(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + BmpDecoderOptions options = new() { RleSkippedPixelHandling = RleSkippedPixelHandling.FirstColorOfPalette }; + using Image image = provider.GetImage(BmpDecoder.Instance, options); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + } + [Theory] [WithFile(RLE8, PixelTypes.Rgba32, false)] - [WithFile(RLE8Inverted, PixelTypes.Rgba32, false)] [WithFile(RLE8, PixelTypes.Rgba32, true)] - [WithFile(RLE8Inverted, PixelTypes.Rgba32, true)] public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit(TestImageProvider provider, bool enforceDiscontiguousBuffers) where TPixel : unmanaged, IPixel { @@ -255,6 +265,25 @@ public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit(TestImageProvider image.CompareToOriginal(provider, MagickReferenceDecoder.Png); } + [Theory] + [WithFile(RLE8Inverted, PixelTypes.Rgba32, false)] + [WithFile(RLE8Inverted, PixelTypes.Rgba32, true)] + public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit_Inverted(TestImageProvider provider, bool enforceDiscontiguousBuffers) + where TPixel : unmanaged, IPixel + { + if (enforceDiscontiguousBuffers) + { + provider.LimitAllocatorBufferCapacity().InBytesSqrt(400); + } + + BmpDecoderOptions options = new() { RleSkippedPixelHandling = RleSkippedPixelHandling.FirstColorOfPalette }; + using Image image = provider.GetImage(BmpDecoder.Instance, options); + image.DebugSave(provider); + + // The Reference decoder does not support decoding compressed bmp which are inverted (with negative height). + image.CompareToReferenceOutput(provider); + } + [Theory] [WithFile(RLE24, PixelTypes.Rgba32, false)] [WithFile(RLE24Cut, PixelTypes.Rgba32, false)] diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_Inverted_Rgba32_RunLengthEncoded-inverted.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_Inverted_Rgba32_RunLengthEncoded-inverted.png new file mode 100644 index 0000000000..1c88282561 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_Inverted_Rgba32_RunLengthEncoded-inverted.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e774cba4dda2fe9d3cdff141e7a8c1de7f3e9c8014093abf8697a34e6cc7144 +size 5379 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDeltaAndEOL_MagickRefDecoder_Rgba32_pal8rlecut.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDeltaAndEOL_MagickRefDecoder_Rgba32_pal8rlecut.png new file mode 100644 index 0000000000..86efdd207e --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_8Bit_WithDeltaAndEOL_MagickRefDecoder_Rgba32_pal8rlecut.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44d281c31cd264dcb812df8cb5dc9d5042e915d64e8013af5577dbfea6cbb1cf +size 3955 From e6a9980daf116f3ecbf30e6b3dea84f4f7b28129 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 30 Mar 2026 10:43:41 +0200 Subject: [PATCH 63/66] Change expectedDefaultConfigurationCount to 12 --- tests/ImageSharp.Tests/ConfigurationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index 3c6c759f82..bd65200844 100644 --- a/tests/ImageSharp.Tests/ConfigurationTests.cs +++ b/tests/ImageSharp.Tests/ConfigurationTests.cs @@ -20,7 +20,7 @@ public class ConfigurationTests public Configuration DefaultConfiguration { get; } - private readonly int expectedDefaultConfigurationCount = 11; + private readonly int expectedDefaultConfigurationCount = 12; public ConfigurationTests() { From 802e275e8e5b2815cfdee7369c4fbc400593e347 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 30 Mar 2026 15:00:13 +0200 Subject: [PATCH 64/66] Use CompareToReferenceOutput for failing bmp tests on linux --- .../Formats/Bmp/BmpDecoderTests.cs | 19 ++++++++++++++----- ...der_CanDecodeBitfields_Rgba32_issue735.png | 3 +++ ...er_CanDecodeBitfields_Rgba32_rgb16-565.png | 3 +++ ...CanDecodeBitfields_Rgba32_rgb16-565pal.png | 3 +++ ...r_CanDecodeBitfields_Rgba32_rgb16bfdef.png | 3 +++ ...oder_CanDecodeBitfields_Rgba32_rgb32bf.png | 3 +++ ...r_CanDecodeBitfields_Rgba32_rgb32bfdef.png | 3 +++ ..._16Bit_Inverted_Rgba32_test16-inverted.png | 3 +++ ...coded_4Bit_WithDelta_Rgba32_pal4rlecut.png | 3 +++ ...oded_4Bit_WithDelta_Rgba32_pal4rletrns.png | 3 +++ ...it_WithDelta_Rgba32_rle4-delta-320x240.png | 3 +++ 11 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_issue735.png create mode 100644 tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565.png create mode 100644 tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565pal.png create mode 100644 tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16bfdef.png create mode 100644 tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bf.png create mode 100644 tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bfdef.png create mode 100644 tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_16Bit_Inverted_Rgba32_test16-inverted.png create mode 100644 tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rlecut.png create mode 100644 tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rletrns.png create mode 100644 tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_rle4-delta-320x240.png diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index 3b9ab22ad4..e85c6bcdf7 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -91,13 +91,22 @@ public void BmpDecoder_CanDecodeBitfields(TestImageProvider prov { using Image image = provider.GetImage(BmpDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider); + image.CompareToReferenceOutput(provider); } [Theory] [WithFile(Bit16Inverted, PixelTypes.Rgba32)] + public void BmpDecoder_CanDecode_16Bit_Inverted(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(BmpDecoder.Instance); + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + } + + [Theory] [WithFile(Bit8Inverted, PixelTypes.Rgba32)] - public void BmpDecoder_CanDecode_Inverted(TestImageProvider provider) + public void BmpDecoder_CanDecode_8Bit_Inverted(TestImageProvider provider) where TPixel : unmanaged, IPixel { using Image image = provider.GetImage(BmpDecoder.Instance); @@ -156,7 +165,7 @@ public void BmpDecoder_CanDecode_16Bit(TestImageProvider provide { using Image image = provider.GetImage(BmpDecoder.Instance); image.DebugSave(provider); - image.CompareToOriginal(provider); + image.CompareToOriginal(provider, new SystemDrawingReferenceDecoder(BmpFormat.Instance)); } [Theory] @@ -186,12 +195,12 @@ public void BmpDecoder_CanDecode_32BitV4Header_Fast(TestImageProvider(TestImageProvider provider) where TPixel : unmanaged, IPixel { - RleSkippedPixelHandling skippedPixelHandling = TestEnvironment.IsWindows ? RleSkippedPixelHandling.Black : RleSkippedPixelHandling.FirstColorOfPalette; + RleSkippedPixelHandling skippedPixelHandling = RleSkippedPixelHandling.Black; BmpDecoderOptions options = new() { RleSkippedPixelHandling = skippedPixelHandling }; using Image image = provider.GetImage(BmpDecoder.Instance, options); image.DebugSave(provider); - image.CompareToOriginal(provider); + image.CompareToReferenceOutput(provider); } [Theory] diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_issue735.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_issue735.png new file mode 100644 index 0000000000..48281cebc0 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_issue735.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:15de93db72e2de0ad1a21b67931df57c1a129f062fe1ed3cb7f083761af4fe36 +size 61931 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565.png new file mode 100644 index 0000000000..2e44b7ec58 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a568f9f12adc5606e5b74d25a4bf083e2b565d73635704e205400d5a073fcd3 +size 9895 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565pal.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565pal.png new file mode 100644 index 0000000000..2e44b7ec58 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16-565pal.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a568f9f12adc5606e5b74d25a4bf083e2b565d73635704e205400d5a073fcd3 +size 9895 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16bfdef.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16bfdef.png new file mode 100644 index 0000000000..e91c987b0d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb16bfdef.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c05e0b14a389d11f531d4238900106a81f25a741a55bbbcd160b8b67eb32adb6 +size 7019 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bf.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bf.png new file mode 100644 index 0000000000..71e2079d03 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bf.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d577ac117b8260ad08e6f051c949d9187f04699eed0f063476aaf42eada2366 +size 20640 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bfdef.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bfdef.png new file mode 100644 index 0000000000..71e2079d03 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecodeBitfields_Rgba32_rgb32bfdef.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d577ac117b8260ad08e6f051c949d9187f04699eed0f063476aaf42eada2366 +size 20640 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_16Bit_Inverted_Rgba32_test16-inverted.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_16Bit_Inverted_Rgba32_test16-inverted.png new file mode 100644 index 0000000000..7cf41fb5bc --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_16Bit_Inverted_Rgba32_test16-inverted.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53a33da2c1a84c0e8426e1a2ba24e2fe2de17a7bc75760184fb10bf6dc96954a +size 7896 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rlecut.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rlecut.png new file mode 100644 index 0000000000..db7f30b415 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rlecut.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0c97446dec6af009423dcae431c44d23963179c3c3bfbeaf3f3b52022a33f68 +size 2414 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rletrns.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rletrns.png new file mode 100644 index 0000000000..c2959addd8 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_pal4rletrns.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a651b81764696cd4fe42113d5ac26006f54820d2df8b2103793e82c66597b2b0 +size 2837 diff --git a/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_rle4-delta-320x240.png b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_rle4-delta-320x240.png new file mode 100644 index 0000000000..0e52f53326 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/BmpDecoderTests/BmpDecoder_CanDecode_RunLengthEncoded_4Bit_WithDelta_Rgba32_rle4-delta-320x240.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55672d405a3d085c72cfc9fb26bc3955bf858e525c54c6c4505f093f011f378d +size 3188 From fccd5b55d99b8739de3262515313eba36d62ba49 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 30 Mar 2026 17:23:40 +0200 Subject: [PATCH 65/66] Remove unused variables --- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 40 +++++++------------- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 3 -- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 126c4c8f69..d69cd78e2e 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -371,30 +371,9 @@ protected override ImageInfo Identify(BufferedReadStream stream, CancellationTok { ExrHeaderAttributes header = this.ReadExrHeader(stream); - int bitsPerPixel = this.CalculateBitsPerPixel(); - return new ImageInfo(new Size(header.DataWindow.XMax, header.DataWindow.YMax), this.metadata); } - 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) @@ -475,8 +454,8 @@ private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream) // Next three bytes contain info's about the image. byte flagsByte0 = (byte)stream.ReadByte(); - byte flagsByte1 = (byte)stream.ReadByte(); - byte flagsByte2 = (byte)stream.ReadByte(); + stream.ReadByte(); + stream.ReadByte(); if ((flagsByte0 & (1 << 1)) != 0) { ExrThrowHelper.ThrowNotSupported("Decoding tiled exr images is not supported yet!"); @@ -587,6 +566,11 @@ private ExrHeaderAttributes ParseHeaderAttributes(BufferedReadStream stream) ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the lineOrder attribute is missing!"); } + if (!aspectRatio.HasValue) + { + ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the aspectRatio attribute is missing!"); + } + if (!screenWindowWidth.HasValue) { ExrThrowHelper.ThrowInvalidImageContentException("Invalid exr image header, the screenWindowWidth attribute is missing!"); @@ -648,7 +632,7 @@ private List ReadChannelList(BufferedReadStream stream, int attr } // Last byte should be a null byte. - int byteRead = stream.ReadByte(); + stream.ReadByte(); return channels; } @@ -662,9 +646,11 @@ private ExrChannelInfo ReadChannelInfo(BufferedReadStream stream, out int bytesR bytesRead += 4; byte pLinear = (byte)stream.ReadByte(); - byte reserved0 = (byte)stream.ReadByte(); - byte reserved1 = (byte)stream.ReadByte(); - byte reserved2 = (byte)stream.ReadByte(); + + // Next 3 bytes are reserved bytes and not use. + stream.ReadByte(); + stream.ReadByte(); + stream.ReadByte(); bytesRead += 4; int xSampling = this.ReadSignedInteger(stream); diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index 7a66dfac26..be2b7e5ffe 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -122,8 +122,6 @@ public void Encode(Image image, Stream stream, CancellationToken // Next is offsets table to each pixel row, which will be written after the pixel data was written. int bytesPerChannel = this.pixelType == ExrPixelType.Half ? 2 : 4; - int numberOfChannels = 3; - uint rowSizeBytes = (uint)(width * numberOfChannels * bytesPerChannel); ulong startOfRowOffsetData = (ulong)stream.Position; stream.Position += 8 * height; @@ -162,7 +160,6 @@ private ulong[] EncodeFloatingPointPixelData( uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width); uint rowsPerBlock = ExrUtils.RowsPerBlock(compression); uint bytesPerBlock = bytesPerRow * rowsPerBlock; - int channelCount = channels.Count; using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3, AllocationOptions.Clean); using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); From 84182c5aafa253f402be61816d3a1b8a3754d61c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 30 Mar 2026 17:35:25 +0200 Subject: [PATCH 66/66] Remove more unused variables --- .../Exr/Compression/Decompressors/B44ExrCompression.cs | 2 +- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs index bf6d9929e2..e5b735a395 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs @@ -18,7 +18,7 @@ internal class B44ExrCompression : ExrBaseDecompressor private readonly byte[] scratch = new byte[14]; - private ushort[] s = new ushort[16]; + private readonly ushort[] s = new ushort[16]; private readonly IMemoryOwner tmpBuffer; diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index be2b7e5ffe..6f11bd9f4c 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -121,7 +121,6 @@ public void Encode(Image image, Stream stream, CancellationToken this.WriteHeader(stream, header); // Next is offsets table to each pixel row, which will be written after the pixel data was written. - int bytesPerChannel = this.pixelType == ExrPixelType.Half ? 2 : 4; ulong startOfRowOffsetData = (ulong)stream.Position; stream.Position += 8 * height; @@ -238,7 +237,6 @@ private ulong[] EncodeUnsignedIntPixelData( uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width); uint rowsPerBlock = ExrUtils.RowsPerBlock(compression); uint bytesPerBlock = bytesPerRow * rowsPerBlock; - int channelCount = channels.Count; using IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(width * 3, AllocationOptions.Clean); using IMemoryOwner rowBlockBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock, AllocationOptions.Clean); @@ -379,8 +377,6 @@ private static void WriteUnsignedIntRow(Span buffer, int width, Span private void WriteRowOffsets(Stream stream, int height, ulong[] rowOffsets) { - ulong startOfRowOffsetData = (ulong)stream.Position; - ulong offset = startOfRowOffsetData; for (int i = 0; i < height; i++) { BinaryPrimitives.WriteUInt64LittleEndian(this.buffer, rowOffsets[i]);