diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 9dd333d92f..ac810fff78 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -166,7 +166,8 @@ internal sealed class ExrDecoderCore : ImageDecoderCore int height = this.Height; int channelCount = this.Channels.Count; - using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); + // EXR can omit color channels. Initialize their planes once so absent channels remain black on every row. + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean); using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); Span redPixelData = rowBuffer.GetSpan()[..width]; @@ -254,7 +255,8 @@ internal sealed class ExrDecoderCore : ImageDecoderCore int height = this.Height; int channelCount = this.Channels.Count; - using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4); + // EXR can omit color channels. Initialize their planes once so absent channels remain black on every row. + using IMemoryOwner rowBuffer = this.memoryAllocator.Allocate(width * 4, AllocationOptions.Clean); using IMemoryOwner decompressedPixelDataBuffer = this.memoryAllocator.Allocate((int)bytesPerBlock); Span decompressedPixelData = decompressedPixelDataBuffer.GetSpan(); Span redPixelData = rowBuffer.GetSpan()[..width]; diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs index 23d9ccedd4..e102ab8076 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs @@ -3,8 +3,12 @@ using System.Buffers.Binary; using System.IO.Compression; +using System.Numerics; using System.Text; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.Memory; namespace SixLabors.ImageSharp.Tests.Formats.Exr; @@ -15,11 +19,48 @@ public class ExrZipDecoderTests [Fact] public void Decode_ShortInflatedBlock_Throws() { - byte[] data = BuildExr(ZlibCompress(new byte[8])); + byte[] data = BuildExr(ZlibCompress(new byte[8]), ExrPixelType.Float, 2); Assert.Throws(() => Image.Load(data)); } + /// + /// Missing color channels must not inherit the allocator's previous contents. + /// + /// The stored sample type. + /// The ZIP compression code. + [Theory] + [InlineData(ExrPixelType.Half, 2)] + [InlineData(ExrPixelType.Float, 2)] + [InlineData(ExrPixelType.UnsignedInt, 2)] + [InlineData(ExrPixelType.Half, 3)] + [InlineData(ExrPixelType.Float, 3)] + [InlineData(ExrPixelType.UnsignedInt, 3)] + public void Decode_SingleRedChannel_InitializesMissingColorChannels(ExrPixelType pixelType, byte compression) + { + byte[] predicted = new byte[256 * (pixelType == ExrPixelType.Half ? 2 : 4)]; + + // A zero first byte followed by 128-valued differences reconstructs an all-zero sample plane. + predicted.AsSpan(1).Fill(128); + byte[] data = BuildExr(ZlibCompress(predicted), pixelType, compression); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = new TestMemoryAllocator(0x3F); + DecoderOptions options = new() { Configuration = configuration }; + + using Image image = Image.Load(options, data); + Assert.Equal(new Size(256, 1), image.Size); + + for (int x = 0; x < image.Width; x++) + { + Assert.Equal(new Vector4(0, 0, 0, 1), image[x, 0].ToVector4()); + } + } + + /// + /// Compresses the predictor bytes for a scanline block. + /// + /// The predictor bytes. + /// The zlib stream. private static byte[] ZlibCompress(byte[] data) { using MemoryStream output = new(); @@ -31,7 +72,14 @@ public class ExrZipDecoderTests return output.ToArray(); } - private static byte[] BuildExr(byte[] compressed) + /// + /// Builds a single-row EXR containing only the red channel. + /// + /// The compressed scanline bytes. + /// The stored sample type. + /// The ZIP compression code. + /// The encoded image. + private static byte[] BuildExr(byte[] compressed, ExrPixelType pixelType, byte compression) { const int width = 256; const int height = 1; @@ -47,7 +95,7 @@ public class ExrZipDecoderTests using (BinaryWriter channelWriter = new(channelStream)) { WriteString(channelWriter, "R"); - channelWriter.Write(2); + channelWriter.Write((int)pixelType); channelWriter.Write((byte)0); channelWriter.Write(new byte[] { 0, 0, 0 }); channelWriter.Write(1); @@ -57,7 +105,7 @@ public class ExrZipDecoderTests WriteAttribute(writer, "channels", "chlist", channelStream.ToArray()); } - WriteAttribute(writer, "compression", "compression", [2]); + WriteAttribute(writer, "compression", "compression", [compression]); using (MemoryStream boxStream = new()) using (BinaryWriter boxWriter = new(boxStream))