From 39b13a46c69e495966109dc09a9ebd652635560a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Thu, 23 Apr 2026 20:04:36 +0200 Subject: [PATCH 01/13] Initial version decode Pxr24 compression --- .../Decompressors/B44ExrCompression.cs | 1 + .../Decompressors/Pxr24Compression.cs | 102 ++++++++++++++++++ .../Exr/Compression/ExrDecompressorFactory.cs | 1 + src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 2 +- 4 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs index aa4944649c..3c4e6b1483 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs @@ -72,6 +72,7 @@ internal class B44ExrCompression : ExrBaseDecompressor ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!"); } + // Check if 3-byte encoded flat field. if (this.scratch[2] >= 13 << 2) { Unpack3(this.scratch, this.s); diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs new file mode 100644 index 0000000000..9e97fd0ad5 --- /dev/null +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs @@ -0,0 +1,102 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.IO.Compression; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; + +internal class Pxr24Compression : ExrBaseDecompressor +{ + private readonly IMemoryOwner tmpBuffer; + + private readonly uint rowsPerBlock; + + private readonly int channelCount; + + private readonly int width; + + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator. + /// The bytes per pixel row block. + /// The bytes per pixel row. + public Pxr24Compression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount) + : base(allocator, bytesPerBlock, bytesPerRow) + { + this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + this.rowsPerBlock = rowsPerBlock; + this.width = width; + this.channelCount = channelCount; + } + + /// + public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + { + Span uncompressed = this.tmpBuffer.GetSpan(); + Span outputBuffer = MemoryMarshal.Cast(buffer); + + long pos = stream.Position; + using ZlibInflateStream inflateStream = new( + stream, + () => + { + int left = (int)(compressedBytes - (stream.Position - pos)); + return left > 0 ? left : 0; + }); + inflateStream.AllocateNewBytes((int)this.BytesPerBlock, true); + using DeflateStream dataStream = inflateStream.CompressedStream!; + + int totalRead = 0; + while (totalRead < buffer.Length) + { + int bytesRead = dataStream.Read(uncompressed, totalRead, buffer.Length - totalRead); + if (bytesRead <= 0) + { + break; + } + + totalRead += bytesRead; + } + + if (totalRead == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed image data!"); + } + + int lastIn = 0; + int outputOffset = 0; + for (int y = 0; y < this.rowsPerBlock; y++) + { + for (int c = 0; c < this.channelCount; c++) + { + int offsetT1 = lastIn; + lastIn += this.width; + int offsetT2 = lastIn; + lastIn += this.width; + uint pixel = 0; + for (int x = 0; x < this.width; x++) + { + uint t1 = uncompressed[offsetT1]; + uint t2 = uncompressed[offsetT2]; + uint diff = (t1 << 8) | t2; + + pixel += diff; + outputBuffer[outputOffset] = (ushort)pixel; + + offsetT1++; + offsetT2++; + outputOffset++; + } + } + } + } + + /// + protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose(); +} diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs index 62519666b5..bd8df9230b 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs @@ -37,6 +37,7 @@ internal static class ExrDecompressorFactory ExrCompression.Zip => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow), ExrCompression.RunLengthEncoded => new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow), ExrCompression.B44 => new B44ExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount), + ExrCompression.Pxr24 => new Pxr24Compression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount), _ => throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)), }; } diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 0b75154c2c..ddfbcdc186 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -846,7 +846,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore /// True if the compression is supported; otherwise, false>. private bool IsSupportedCompression() => this.Compression switch { - ExrCompression.None or ExrCompression.Zip or ExrCompression.Zips or ExrCompression.RunLengthEncoded or ExrCompression.B44 => true, + ExrCompression.None or ExrCompression.Zip or ExrCompression.Zips or ExrCompression.RunLengthEncoded or ExrCompression.B44 or ExrCompression.Pxr24 => true, _ => false, }; From d805b9b50c96705cacc3d006715cbc7b9d927531 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 24 Apr 2026 11:25:28 +0200 Subject: [PATCH 02/13] Add test case for pxr24 compressed image --- tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs | 10 ++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Exr/Calliphora_half_pxr24.exr | 3 +++ 3 files changed, 14 insertions(+) create mode 100644 tests/Images/Input/Exr/Calliphora_half_pxr24.exr diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index b81dba7555..dd958e87ab 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -119,6 +119,16 @@ public class ExrDecoderTests image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); } + [Theory] + [WithFile(TestImages.Exr.Pxr24Half, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Pxr24Compressed(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); + } + [Theory] [WithFile(TestImages.Exr.B44, PixelTypes.Rgba32)] public void ExrDecoder_CanDecode_B44Compressed(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 1a8cf948e8..90adae34cc 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1395,6 +1395,7 @@ public static class TestImages 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 Pxr24Half = "Exr/Calliphora_half_pxr24.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_half_pxr24.exr b/tests/Images/Input/Exr/Calliphora_half_pxr24.exr new file mode 100644 index 0000000000..17a90699b0 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_half_pxr24.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:927778a73832b5ad68473e46df6be52076d98294271f2cd0ead66691db41b7bf +size 195063 From 344d49d86fb172409430c31b03cc3a3b1798ba2c Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 24 Apr 2026 13:26:23 +0200 Subject: [PATCH 03/13] Use Width Property from ExrBaseDecompressor --- .../Compressors/NoneExrCompressor.cs | 5 +-- .../Compressors/ZipExrCompressor.cs | 5 +-- .../Decompressors/B44ExrCompression.cs | 33 +++++++++---------- .../Decompressors/NoneExrCompression.cs | 5 +-- .../Decompressors/Pxr24Compression.cs | 17 ++++++---- .../Decompressors/RunLengthExrCompression.cs | 5 +-- .../Decompressors/ZipExrCompression.cs | 5 +-- .../Exr/Compression/ExrBaseCompression.cs | 4 ++- .../Exr/Compression/ExrBaseDecompressor.cs | 5 +-- .../Exr/Compression/ExrCompressorFactory.cs | 8 +++-- .../Exr/Compression/ExrDecompressorFactory.cs | 8 ++--- .../Formats/Exr/ExrBaseCompressor.cs | 5 +-- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 4 +-- 13 files changed, 60 insertions(+), 49 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs index 58768e990f..c1240667e7 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs @@ -17,8 +17,9 @@ internal class NoneExrCompressor : ExrBaseCompressor /// The memory allocator. /// Bytes per row block. /// Bytes per pixel row. - public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) - : base(output, allocator, bytesPerBlock, bytesPerRow) + /// The witdh of one row in pixels. + public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) + : base(output, allocator, bytesPerBlock, bytesPerRow, width) { } diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs index ef7285da0c..bf5f078585 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -24,9 +24,10 @@ internal class ZipExrCompressor : ExrBaseCompressor /// The memory allocator. /// The bytes per block. /// The bytes per row. + /// The witdh of one row in pixels. /// The compression level for deflate compression. - public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, DeflateCompressionLevel compressionLevel) - : base(output, allocator, bytesPerBlock, bytesPerRow) + public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width, DeflateCompressionLevel compressionLevel) + : base(output, allocator, bytesPerBlock, bytesPerRow, width) { this.compressionLevel = compressionLevel; this.buffer = allocator.Allocate((int)bytesPerBlock); diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs index 3c4e6b1483..64774537fa 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs @@ -13,8 +13,6 @@ namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; /// internal class B44ExrCompression : ExrBaseDecompressor { - private readonly int width; - private readonly uint rowsPerBlock; private readonly int channelCount; @@ -35,9 +33,8 @@ internal class B44ExrCompression : ExrBaseDecompressor /// The width of a pixel row in pixels. /// The number of channels of the image. public B44ExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount) - : base(allocator, bytesPerBlock, bytesPerRow) + : base(allocator, bytesPerBlock, bytesPerRow, width) { - this.width = width; this.rowsPerBlock = rowsPerBlock; this.channelCount = channelCount; this.tmpBuffer = allocator.Allocate((int)(width * rowsPerBlock * channelCount)); @@ -54,17 +51,17 @@ internal class B44ExrCompression : ExrBaseDecompressor { 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; + 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) + for (int x = 0; x < this.Width && bytesLeft > 0; x += 4) { int bytesRead = stream.Read(this.scratch, 0, 3); if (bytesRead == 0) @@ -90,7 +87,7 @@ internal class B44ExrCompression : ExrBaseDecompressor bytesLeft -= 14; } - int n = x + 3 < this.width ? 4 : this.width - x; + int n = x + 3 < this.Width ? 4 : this.Width - x; if (y + 3 < this.rowsPerBlock) { this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]); @@ -125,16 +122,16 @@ internal class B44ExrCompression : ExrBaseDecompressor // 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); + 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[offsetOutput..]); - offsetOutput += this.width; + decompressed.Slice(offsetDecompressed + (i * blockSize), this.Width).CopyTo(outputBuffer[offsetOutput..]); + offsetOutput += this.Width; } - offsetDecompressed += this.width; + offsetDecompressed += this.Width; } } diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs index c15ffbe325..1b045bdeb7 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs @@ -17,8 +17,9 @@ internal class NoneExrCompression : ExrBaseDecompressor /// The memory allocator. /// The bytes per pixel row block. /// The bytes per pixel row. - public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) - : base(allocator, bytesPerBlock, bytesPerRow) + /// The number of pixels per row. + public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) + : base(allocator, bytesPerBlock, bytesPerRow, width) { } diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs index 9e97fd0ad5..d720c190f9 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs @@ -10,6 +10,9 @@ using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; +/// +/// Implementation of PXR24 decompressor for EXR image data. +/// internal class Pxr24Compression : ExrBaseDecompressor { private readonly IMemoryOwner tmpBuffer; @@ -18,20 +21,20 @@ internal class Pxr24Compression : ExrBaseDecompressor private readonly int channelCount; - private readonly int width; - /// /// Initializes a new instance of the class. /// /// The memory allocator. /// The bytes per pixel row block. /// The bytes per pixel row. + /// The pixel rows per block. + /// The witdh of one row in pixels. + /// The number of channels for a pixel. public Pxr24Compression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount) - : base(allocator, bytesPerBlock, bytesPerRow) + : base(allocator, bytesPerBlock, bytesPerRow, width) { this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); this.rowsPerBlock = rowsPerBlock; - this.width = width; this.channelCount = channelCount; } @@ -76,11 +79,11 @@ internal class Pxr24Compression : ExrBaseDecompressor for (int c = 0; c < this.channelCount; c++) { int offsetT1 = lastIn; - lastIn += this.width; + lastIn += this.Width; int offsetT2 = lastIn; - lastIn += this.width; + lastIn += this.Width; uint pixel = 0; - for (int x = 0; x < this.width; x++) + for (int x = 0; x < this.Width; x++) { uint t1 = uncompressed[offsetT1]; uint t2 = uncompressed[offsetT2]; diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs index 489493d82f..29782ae03d 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs @@ -22,8 +22,9 @@ internal class RunLengthExrCompression : ExrBaseDecompressor /// The memory allocator. /// The bytes per pixel row block. /// The bytes per row. - public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) - : base(allocator, bytesPerBlock, bytesPerRow) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + /// The witdh of one row in pixels. + public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) + : base(allocator, bytesPerBlock, bytesPerRow, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); /// public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs index dafe3d8321..4333f53cb5 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs @@ -22,8 +22,9 @@ internal class ZipExrCompression : ExrBaseDecompressor /// The memory allocator. /// The bytes per pixel row block. /// The bytes per pixel row. - public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) - : base(allocator, bytesPerBlock, bytesPerRow) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + /// The witdh of one row in pixels. + public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) + : base(allocator, bytesPerBlock, bytesPerRow, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); /// public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs index b116dffc65..5912fcdecf 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs @@ -18,11 +18,13 @@ internal abstract class ExrBaseCompression : IDisposable /// The memory allocator. /// The bytes per block. /// The bytes per row. - protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) + /// The number of pixels of a row. + protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) { this.Allocator = allocator; this.BytesPerBlock = bytesPerBlock; this.BytesPerRow = bytesPerRow; + this.Width = width; } /// diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs index efe3c7877f..000e6ecea8 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs @@ -17,8 +17,9 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression /// The memory allocator. /// The bytes per row block. /// The bytes per row. - protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) - : base(allocator, bytesPerBlock, bytesPerRow) + /// The number of pixels per row. + protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) + : base(allocator, bytesPerBlock, bytesPerRow, width) { } diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs index b1b7d2e8e6..497bd492aa 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs @@ -21,6 +21,7 @@ internal static class ExrCompressorFactory /// The output stream. /// The bytes per block. /// The bytes per row. + /// The witdh of one row in pixels. /// The deflate compression level. /// A compressor for EXR image data. public static ExrBaseCompressor Create( @@ -29,11 +30,12 @@ internal static class ExrCompressorFactory Stream output, uint bytesPerBlock, uint bytesPerRow, + int width, DeflateCompressionLevel compressionLevel = DeflateCompressionLevel.DefaultCompression) => method switch { - ExrCompression.None => new NoneExrCompressor(output, allocator, bytesPerBlock, bytesPerRow), - ExrCompression.Zips => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, compressionLevel), - ExrCompression.Zip => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, compressionLevel), + ExrCompression.None => new NoneExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, width), + ExrCompression.Zips => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, width, compressionLevel), + ExrCompression.Zip => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, width, compressionLevel), _ => throw ExrThrowHelper.NotSupportedCompressor(method.ToString()), }; } diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs index bd8df9230b..8123284585 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs @@ -32,10 +32,10 @@ internal static class ExrDecompressorFactory uint rowsPerBlock, int channelCount) => method switch { - ExrCompression.None => new NoneExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow), - ExrCompression.Zips => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow), - ExrCompression.Zip => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow), - ExrCompression.RunLengthEncoded => new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow), + ExrCompression.None => new NoneExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, width), + ExrCompression.Zips => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, width), + ExrCompression.Zip => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, width), + ExrCompression.RunLengthEncoded => new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, width), ExrCompression.B44 => new B44ExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount), ExrCompression.Pxr24 => new Pxr24Compression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount), _ => throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)), diff --git a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs index 1dcdec5be0..83887fd5f1 100644 --- a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs +++ b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs @@ -14,8 +14,9 @@ internal abstract class ExrBaseCompressor : ExrBaseCompression /// The memory allocator. /// Bytes per row block. /// Bytes per pixel row. - protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow) - : base(allocator, bytesPerBlock, bytesPerRow) + /// The number of pixels per row. + protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) + : base(allocator, bytesPerBlock, bytesPerRow, width) => this.Output = output; /// diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index 04ca5e40c8..c6824c6aad 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -181,7 +181,7 @@ internal sealed class ExrEncoderCore Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); Span alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width); - using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow); + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, width); ulong[] rowOffsets = new ulong[height]; for (uint y = 0; y < height; y += rowsPerBlock) @@ -273,7 +273,7 @@ internal sealed class ExrEncoderCore Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); Span alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width); - using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow); + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, width); Rgba128 rgb = default; ulong[] rowOffsets = new ulong[height]; From 95ee73e241186b779a4ecfe35e3cd3c06759b282 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 25 Apr 2026 16:10:57 +0200 Subject: [PATCH 04/13] Implement pxr decompression for pixel type uint and float --- .../Decompressors/Pxr24Compression.cs | 106 +++++++++++++++--- .../Exr/Compression/ExrDecompressorFactory.cs | 5 +- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 20 +++- 3 files changed, 112 insertions(+), 19 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs index d720c190f9..d2fdcfcd16 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.IO.Compression; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Compression.Zlib; +using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -21,6 +22,8 @@ internal class Pxr24Compression : ExrBaseDecompressor private readonly int channelCount; + private readonly ExrPixelType pixelType; + /// /// Initializes a new instance of the class. /// @@ -30,12 +33,14 @@ internal class Pxr24Compression : ExrBaseDecompressor /// The pixel rows per block. /// The witdh of one row in pixels. /// The number of channels for a pixel. - public Pxr24Compression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount) + /// The pixel type. + public Pxr24Compression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount, ExrPixelType pixelType) : base(allocator, bytesPerBlock, bytesPerRow, width) { this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); this.rowsPerBlock = rowsPerBlock; this.channelCount = channelCount; + this.pixelType = pixelType; } /// @@ -78,23 +83,94 @@ internal class Pxr24Compression : ExrBaseDecompressor { for (int c = 0; c < this.channelCount; c++) { - int offsetT1 = lastIn; - lastIn += this.Width; - int offsetT2 = lastIn; - lastIn += this.Width; - uint pixel = 0; - for (int x = 0; x < this.Width; x++) + switch (this.pixelType) { - uint t1 = uncompressed[offsetT1]; - uint t2 = uncompressed[offsetT2]; - uint diff = (t1 << 8) | t2; + case ExrPixelType.UnsignedInt: + { + int offsetT0 = lastIn; + lastIn += this.Width; + int offsetT1 = lastIn; + lastIn += this.Width; + int offsetT2 = lastIn; + lastIn += this.Width; + int offsetT3 = lastIn; + lastIn += this.Width; + + uint pixel = 0; + for (int x = 0; x < this.Width; x++) + { + uint t0 = uncompressed[offsetT0]; + uint t1 = uncompressed[offsetT1]; + uint t2 = uncompressed[offsetT2]; + uint t3 = uncompressed[offsetT3]; + uint diff = (t0 << 24) | (t1 << 16) | (t2 << 8) | t3; + + pixel += diff; + outputBuffer[outputOffset] = (ushort)pixel; + + offsetT0++; + offsetT1++; + offsetT2++; + offsetT3++; + outputOffset++; + } + + break; + } + + case ExrPixelType.Half: + { + int offsetT0 = lastIn; + lastIn += this.Width; + int offsetT1 = lastIn; + lastIn += this.Width; + + uint pixel = 0; + for (int x = 0; x < this.Width; x++) + { + uint t0 = uncompressed[offsetT0]; + uint t1 = uncompressed[offsetT1]; + uint diff = (t0 << 8) | t1; + + pixel += diff; + outputBuffer[outputOffset] = (ushort)pixel; + + offsetT0++; + offsetT1++; + outputOffset++; + } + + break; + } + + case ExrPixelType.Float: + { + int offsetT0 = lastIn; + lastIn += this.Width; + int offsetT1 = lastIn; + lastIn += this.Width; + int offsetT2 = lastIn; + lastIn += this.Width; + + uint pixel = 0; + for (int x = 0; x < this.Width; x++) + { + uint t0 = uncompressed[offsetT0]; + uint t1 = uncompressed[offsetT1]; + uint t2 = uncompressed[offsetT2]; + uint diff = (t0 << 24) | (t1 << 16) | (t2 << 8); + + pixel += diff; + outputBuffer[outputOffset] = (ushort)pixel; - pixel += diff; - outputBuffer[outputOffset] = (ushort)pixel; + offsetT0++; + offsetT1++; + offsetT2++; + outputOffset++; + } - offsetT1++; - offsetT2++; - outputOffset++; + break; + } } } } diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs index 8123284585..6f63a932b7 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs @@ -30,14 +30,15 @@ internal static class ExrDecompressorFactory uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, - int channelCount) => method switch + int channelCount, + ExrPixelType pixelType) => method switch { ExrCompression.None => new NoneExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, width), ExrCompression.Zips => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, width), ExrCompression.Zip => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, width), ExrCompression.RunLengthEncoded => new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, width), ExrCompression.B44 => new B44ExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount), - ExrCompression.Pxr24 => new Pxr24Compression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount), + ExrCompression.Pxr24 => new Pxr24Compression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount, pixelType), _ => throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)), }; } diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index ddfbcdc186..2e60f3a5b9 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -154,7 +154,15 @@ internal sealed class ExrDecoderCore : ImageDecoderCore 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, bytesPerBlock, bytesPerRow, rowsPerBlock, channelCount); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create( + this.Compression, + this.memoryAllocator, + width, + bytesPerBlock, + bytesPerRow, + rowsPerBlock, + channelCount, + this.PixelType); int decodedRows = 0; while (decodedRows < height) @@ -219,7 +227,15 @@ internal sealed class ExrDecoderCore : ImageDecoderCore 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, bytesPerBlock, bytesPerRow, rowsPerBlock, channelCount); + using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create( + this.Compression, + this.memoryAllocator, + width, + bytesPerBlock, + bytesPerRow, + rowsPerBlock, + channelCount, + this.PixelType); int decodedRows = 0; while (decodedRows < height) From efa1f8bbae992b0d2c8c239564e0ab394dfadb02 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 26 Apr 2026 09:57:15 +0200 Subject: [PATCH 05/13] Add test case for pxr compression with pixel type float --- tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs | 1 + tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Exr/Calliphora_float_pxr24.exr | 3 +++ 3 files changed, 5 insertions(+) create mode 100644 tests/Images/Input/Exr/Calliphora_float_pxr24.exr diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index dd958e87ab..ef2a0100be 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -121,6 +121,7 @@ public class ExrDecoderTests [Theory] [WithFile(TestImages.Exr.Pxr24Half, PixelTypes.Rgba32)] + [WithFile(TestImages.Exr.Pxr24Float, PixelTypes.Rgba32)] public void ExrDecoder_CanDecode_Pxr24Compressed(TestImageProvider provider) where TPixel : unmanaged, IPixel { diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 90adae34cc..b08e26dc7d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1396,6 +1396,7 @@ public static class TestImages public const string Rle = "Exr/Calliphora_rle.exr"; public const string B44 = "Exr/Calliphora_b44.exr"; public const string Pxr24Half = "Exr/Calliphora_half_pxr24.exr"; + public const string Pxr24Float = "Exr/Calliphora_float_pxr24.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_float_pxr24.exr b/tests/Images/Input/Exr/Calliphora_float_pxr24.exr new file mode 100644 index 0000000000..3c5d61e535 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_float_pxr24.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ca6c29eb36395b923298d2bf4ba1f73fd7f081f7b1af2e72b94b92ad2acb638b +size 226997 From e8c9d6db9d2c57dec674933c0a66f6866204b396 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 26 Apr 2026 11:01:36 +0200 Subject: [PATCH 06/13] Add test case for pxr compression with pixel type uint --- .../ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs | 12 ++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + ...xrPixelType_Uint_Rgba32_Calliphora_uint_pxr24.png | 3 +++ tests/Images/Input/Exr/Calliphora_uint_pxr24.exr | 3 +++ 4 files changed, 19 insertions(+) create mode 100644 tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Pxr24Compressed_ExrPixelType_Uint_Rgba32_Calliphora_uint_pxr24.png create mode 100644 tests/Images/Input/Exr/Calliphora_uint_pxr24.exr diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs index ef2a0100be..febdc9dae7 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -130,6 +130,18 @@ public class ExrDecoderTests image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); } + [Theory] + [WithFile(TestImages.Exr.Pxr24Uint, PixelTypes.Rgba32)] + public void ExrDecoder_CanDecode_Pxr24Compressed_ExrPixelType_Uint(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(ExrDecoder.Instance); + image.DebugSave(provider); + + // Compare to Reference here instead using the reference decoder, since uint pixel type is not supported by the Reference decoder. + image.CompareToReferenceOutput(provider); + } + [Theory] [WithFile(TestImages.Exr.B44, PixelTypes.Rgba32)] public void ExrDecoder_CanDecode_B44Compressed(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index b08e26dc7d..4f15650cd2 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1397,6 +1397,7 @@ public static class TestImages public const string B44 = "Exr/Calliphora_b44.exr"; public const string Pxr24Half = "Exr/Calliphora_half_pxr24.exr"; public const string Pxr24Float = "Exr/Calliphora_float_pxr24.exr"; + public const string Pxr24Uint = "Exr/Calliphora_uint_pxr24.exr"; public const string Rgb = "Exr/Calliphora_rgb.exr"; public const string Gray = "Exr/Calliphora_gray.exr"; } diff --git a/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Pxr24Compressed_ExrPixelType_Uint_Rgba32_Calliphora_uint_pxr24.png b/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Pxr24Compressed_ExrPixelType_Uint_Rgba32_Calliphora_uint_pxr24.png new file mode 100644 index 0000000000..8f31f0d8d0 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Pxr24Compressed_ExrPixelType_Uint_Rgba32_Calliphora_uint_pxr24.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f66bf45b5d80e412314341567b8cf240d56b5872f01ce9f3b0d6034d85e8e942 +size 163327 diff --git a/tests/Images/Input/Exr/Calliphora_uint_pxr24.exr b/tests/Images/Input/Exr/Calliphora_uint_pxr24.exr new file mode 100644 index 0000000000..fd858abb04 --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_uint_pxr24.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc72685224ba818ac77d914f5e4c91162503f88f775e16b5c745baef6bc7b790 +size 187914 From e917fd317afb08a575380c543da367fc52906e4d Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 26 Apr 2026 11:30:08 +0200 Subject: [PATCH 07/13] Fix issues with pxr and pixel type uint and float --- .../Exr/Compression/Decompressors/Pxr24Compression.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs index d2fdcfcd16..e15ac4e845 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs @@ -47,7 +47,9 @@ internal class Pxr24Compression : ExrBaseDecompressor public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) { Span uncompressed = this.tmpBuffer.GetSpan(); - Span outputBuffer = MemoryMarshal.Cast(buffer); + Span outputBufferHalf = MemoryMarshal.Cast(buffer); + Span outputBufferFloat = MemoryMarshal.Cast(buffer); + Span outputBufferUint = MemoryMarshal.Cast(buffer); long pos = stream.Position; using ZlibInflateStream inflateStream = new( @@ -106,7 +108,7 @@ internal class Pxr24Compression : ExrBaseDecompressor uint diff = (t0 << 24) | (t1 << 16) | (t2 << 8) | t3; pixel += diff; - outputBuffer[outputOffset] = (ushort)pixel; + outputBufferUint[outputOffset] = pixel; offsetT0++; offsetT1++; @@ -133,7 +135,7 @@ internal class Pxr24Compression : ExrBaseDecompressor uint diff = (t0 << 8) | t1; pixel += diff; - outputBuffer[outputOffset] = (ushort)pixel; + outputBufferHalf[outputOffset] = (ushort)pixel; offsetT0++; offsetT1++; @@ -161,7 +163,7 @@ internal class Pxr24Compression : ExrBaseDecompressor uint diff = (t0 << 24) | (t1 << 16) | (t2 << 8); pixel += diff; - outputBuffer[outputOffset] = (ushort)pixel; + outputBufferFloat[outputOffset] = pixel; offsetT0++; offsetT1++; From a85fe63d312b401058b3636ab3c3b50ef3780587 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 26 Apr 2026 14:50:52 +0200 Subject: [PATCH 08/13] Move undoing zip compression into base class --- .../Decompressors/Pxr24Compression.cs | 29 +------------ .../Decompressors/ZipExrCompression.cs | 29 +------------ .../Exr/Compression/ExrBaseDecompressor.cs | 43 +++++++++++++++++++ 3 files changed, 47 insertions(+), 54 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs index e15ac4e845..115f455f04 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs @@ -51,33 +51,8 @@ internal class Pxr24Compression : ExrBaseDecompressor Span outputBufferFloat = MemoryMarshal.Cast(buffer); Span outputBufferUint = MemoryMarshal.Cast(buffer); - long pos = stream.Position; - using ZlibInflateStream inflateStream = new( - stream, - () => - { - int left = (int)(compressedBytes - (stream.Position - pos)); - return left > 0 ? left : 0; - }); - inflateStream.AllocateNewBytes((int)this.BytesPerBlock, true); - using DeflateStream dataStream = inflateStream.CompressedStream!; - - int totalRead = 0; - while (totalRead < buffer.Length) - { - int bytesRead = dataStream.Read(uncompressed, totalRead, buffer.Length - totalRead); - if (bytesRead <= 0) - { - break; - } - - totalRead += bytesRead; - } - - if (totalRead == 0) - { - ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed image data!"); - } + uint uncompressedBytes = this.BytesPerBlock; + UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes); int lastIn = 0; int outputOffset = 0; diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs index 4333f53cb5..51f8ab4c38 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs @@ -31,33 +31,8 @@ internal class ZipExrCompression : ExrBaseDecompressor { Span uncompressed = this.tmpBuffer.GetSpan(); - long pos = stream.Position; - using ZlibInflateStream inflateStream = new( - stream, - () => - { - int left = (int)(compressedBytes - (stream.Position - pos)); - return left > 0 ? left : 0; - }); - inflateStream.AllocateNewBytes((int)this.BytesPerBlock, true); - using DeflateStream dataStream = inflateStream.CompressedStream!; - - int totalRead = 0; - while (totalRead < buffer.Length) - { - int bytesRead = dataStream.Read(uncompressed, totalRead, buffer.Length - totalRead); - if (bytesRead <= 0) - { - break; - } - - totalRead += bytesRead; - } - - if (totalRead == 0) - { - ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed image data!"); - } + uint uncompressedBytes = (uint)buffer.Length; + int totalRead = UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes); Reconstruct(uncompressed, (uint)totalRead); Interleave(uncompressed, (uint)totalRead, buffer); diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs index 000e6ecea8..b958982cec 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.IO.Compression; +using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -31,6 +33,47 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression /// The buffer to write the decompressed data to. public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer); + /// + /// Decompresses zip compressed data. + /// + /// The buffered stream to decompress. + /// The compressed bytes. + /// The buffer to write the uncompressed data to. + /// The uncompressed bytes. + /// The total bytes read from the stream. + protected static int UndoZipCompression(BufferedReadStream stream, uint compressedBytes, Span uncompressed, uint uncompressedBytes) + { + long pos = stream.Position; + using ZlibInflateStream inflateStream = new( + stream, + () => + { + int left = (int)(compressedBytes - (stream.Position - pos)); + return left > 0 ? left : 0; + }); + inflateStream.AllocateNewBytes((int)uncompressedBytes, true); + using DeflateStream dataStream = inflateStream.CompressedStream!; + + int totalRead = 0; + while (totalRead < uncompressedBytes) + { + int bytesRead = dataStream.Read(uncompressed, totalRead, (int)uncompressedBytes - totalRead); + if (bytesRead <= 0) + { + break; + } + + totalRead += bytesRead; + } + + if (totalRead == 0) + { + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed image data!"); + } + + return totalRead; + } + /// /// Integrate over all differences to the previous value in order to /// reconstruct sample values. From 7d38e50d3f3ccb6e544716a91ba5f7f62ee476d3 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 26 Apr 2026 14:57:27 +0200 Subject: [PATCH 09/13] Use better test image for float pixel type created with reference encoder --- tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs | 3 +-- tests/ImageSharp.Tests/TestImages.cs | 2 +- tests/Images/Input/Exr/Calliphora_float_uncompressed.exr | 3 +++ tests/Images/Input/Exr/rgb_float32_uncompressed.exr | 3 --- 4 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 tests/Images/Input/Exr/Calliphora_float_uncompressed.exr delete 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 febdc9dae7..1ee0cc582b 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs @@ -36,8 +36,7 @@ public class ExrDecoderTests 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); + image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder); Assert.Equal(ExrPixelType.Float, exrMetaData.PixelType); } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 4f15650cd2..2624d1cdad 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1388,7 +1388,7 @@ public static class TestImages 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 UncompressedFloatRgb = "Exr/Calliphora_float_uncompressed.exr"; public const string UncompressedUintRgb = "Exr/Calliphora_uint32_uncompressed.exr"; public const string UintRgba = "Exr/rgba_uint_uncompressed.exr"; public const string Zip = "Exr/Calliphora_zip.exr"; diff --git a/tests/Images/Input/Exr/Calliphora_float_uncompressed.exr b/tests/Images/Input/Exr/Calliphora_float_uncompressed.exr new file mode 100644 index 0000000000..04a5c035dd --- /dev/null +++ b/tests/Images/Input/Exr/Calliphora_float_uncompressed.exr @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:926996c647c1c54921db0a623e58e5edeb92b447cb4eceb53b8c0e86fd24f11e +size 720281 diff --git a/tests/Images/Input/Exr/rgb_float32_uncompressed.exr b/tests/Images/Input/Exr/rgb_float32_uncompressed.exr deleted file mode 100644 index 489758bb04..0000000000 --- a/tests/Images/Input/Exr/rgb_float32_uncompressed.exr +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5ab8b71824ca384fc2cde1f74a6f34c38169fa328ccc67f17d08333e9de42f55 -size 243558 From 2bf2e9563853127f1d2f55f2e62ef671741f4924 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 26 Apr 2026 15:12:55 +0200 Subject: [PATCH 10/13] Move rowsPerBlock to base compression class --- .../Compressors/NoneExrCompressor.cs | 5 +++-- .../Compressors/ZipExrCompressor.cs | 5 +++-- .../Decompressors/B44ExrCompression.cs | 19 ++++++++----------- .../Decompressors/NoneExrCompression.cs | 5 +++-- .../Decompressors/Pxr24Compression.cs | 9 ++------- .../Decompressors/RunLengthExrCompression.cs | 4 ++-- .../Decompressors/ZipExrCompression.cs | 7 +++---- .../Exr/Compression/ExrBaseCompression.cs | 9 ++++++++- .../Exr/Compression/ExrBaseDecompressor.cs | 5 +++-- .../Exr/Compression/ExrCompressorFactory.cs | 8 +++++--- .../Exr/Compression/ExrDecompressorFactory.cs | 9 +++++---- .../Formats/Exr/ExrBaseCompressor.cs | 5 +++-- src/ImageSharp/Formats/Exr/ExrEncoderCore.cs | 4 ++-- 13 files changed, 50 insertions(+), 44 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs index c1240667e7..8aca0da65e 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs @@ -17,9 +17,10 @@ internal class NoneExrCompressor : ExrBaseCompressor /// The memory allocator. /// Bytes per row block. /// Bytes per pixel row. + /// The pixel rows per block. /// The witdh of one row in pixels. - public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) - : base(output, allocator, bytesPerBlock, bytesPerRow, width) + public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) { } diff --git a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs index bf5f078585..01248cc395 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs @@ -24,10 +24,11 @@ internal class ZipExrCompressor : ExrBaseCompressor /// The memory allocator. /// The bytes per block. /// The bytes per row. + /// The pixel rows per block. /// The witdh of one row in pixels. /// The compression level for deflate compression. - public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width, DeflateCompressionLevel compressionLevel) - : base(output, allocator, bytesPerBlock, bytesPerRow, width) + public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, DeflateCompressionLevel compressionLevel) + : base(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) { this.compressionLevel = compressionLevel; this.buffer = allocator.Allocate((int)bytesPerBlock); diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs index 64774537fa..06df7b6a25 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs @@ -13,8 +13,6 @@ namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors; /// internal class B44ExrCompression : ExrBaseDecompressor { - private readonly uint rowsPerBlock; - private readonly int channelCount; private readonly byte[] scratch = new byte[14]; @@ -29,13 +27,12 @@ internal class B44ExrCompression : ExrBaseDecompressor /// The memory allocator. /// The bytes per pixel row block. /// The bytes per row. - /// The rows per block. + /// The pixel rows per block. /// The width of a pixel row in pixels. /// The number of channels of the image. public B44ExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount) - : base(allocator, bytesPerBlock, bytesPerRow, width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) { - this.rowsPerBlock = rowsPerBlock; this.channelCount = channelCount; this.tmpBuffer = allocator.Allocate((int)(width * rowsPerBlock * channelCount)); } @@ -49,7 +46,7 @@ internal class B44ExrCompression : ExrBaseDecompressor int bytesLeft = (int)compressedBytes; for (int i = 0; i < this.channelCount && bytesLeft > 0; i++) { - for (int y = 0; y < this.rowsPerBlock; y += 4) + for (int y = 0; y < this.RowsPerBlock; y += 4) { Span row0 = decompressed.Slice(outputOffset, this.Width); outputOffset += this.Width; @@ -88,7 +85,7 @@ internal class B44ExrCompression : ExrBaseDecompressor } int n = x + 3 < this.Width ? 4 : this.Width - x; - if (y + 3 < this.rowsPerBlock) + if (y + 3 < this.RowsPerBlock) { this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]); this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]); @@ -98,12 +95,12 @@ internal class B44ExrCompression : ExrBaseDecompressor else { this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]); - if (y + 1 < this.rowsPerBlock) + if (y + 1 < this.RowsPerBlock) { this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]); } - if (y + 2 < this.rowsPerBlock) + if (y + 2 < this.RowsPerBlock) { this.s.AsSpan(8, n).CopyTo(row2[rowOffset..]); } @@ -122,8 +119,8 @@ internal class B44ExrCompression : ExrBaseDecompressor // 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++) + int blockSize = (int)(this.Width * this.RowsPerBlock); + for (int y = 0; y < this.RowsPerBlock; y++) { for (int i = 0; i < this.channelCount; i++) { diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs index 1b045bdeb7..19edb31afe 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs @@ -17,9 +17,10 @@ internal class NoneExrCompression : ExrBaseDecompressor /// The memory allocator. /// The bytes per pixel row block. /// The bytes per pixel row. + /// The pixel rows per block. /// The number of pixels per row. - public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) - : base(allocator, bytesPerBlock, bytesPerRow, width) + public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) { } diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs index 115f455f04..f45b660e7d 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs @@ -2,9 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.IO.Compression; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Exr.Constants; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -18,8 +16,6 @@ internal class Pxr24Compression : ExrBaseDecompressor { private readonly IMemoryOwner tmpBuffer; - private readonly uint rowsPerBlock; - private readonly int channelCount; private readonly ExrPixelType pixelType; @@ -35,10 +31,9 @@ internal class Pxr24Compression : ExrBaseDecompressor /// The number of channels for a pixel. /// The pixel type. public Pxr24Compression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount, ExrPixelType pixelType) - : base(allocator, bytesPerBlock, bytesPerRow, width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) { this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); - this.rowsPerBlock = rowsPerBlock; this.channelCount = channelCount; this.pixelType = pixelType; } @@ -56,7 +51,7 @@ internal class Pxr24Compression : ExrBaseDecompressor int lastIn = 0; int outputOffset = 0; - for (int y = 0; y < this.rowsPerBlock; y++) + for (int y = 0; y < this.RowsPerBlock; y++) { for (int c = 0; c < this.channelCount; c++) { diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs index 29782ae03d..bb3903deb8 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs @@ -23,8 +23,8 @@ internal class RunLengthExrCompression : ExrBaseDecompressor /// The bytes per pixel row block. /// The bytes per row. /// The witdh of one row in pixels. - public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) - : base(allocator, bytesPerBlock, bytesPerRow, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); /// public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs index 51f8ab4c38..8bab76f402 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs @@ -2,8 +2,6 @@ // Licensed under the Six Labors Split License. using System.Buffers; -using System.IO.Compression; -using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; @@ -22,9 +20,10 @@ internal class ZipExrCompression : ExrBaseDecompressor /// The memory allocator. /// The bytes per pixel row block. /// The bytes per pixel row. + /// The pixel rows per block. /// The witdh of one row in pixels. - public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) - : base(allocator, bytesPerBlock, bytesPerRow, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); + public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); /// public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs index 5912fcdecf..72d5be980b 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs @@ -18,12 +18,14 @@ internal abstract class ExrBaseCompression : IDisposable /// The memory allocator. /// The bytes per block. /// The bytes per row. + /// The number of pixel rows per block. /// The number of pixels of a row. - protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) + protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) { this.Allocator = allocator; this.BytesPerBlock = bytesPerBlock; this.BytesPerRow = bytesPerRow; + this.RowsPerBlock = rowsPerBlock; this.Width = width; } @@ -47,6 +49,11 @@ internal abstract class ExrBaseCompression : IDisposable /// public uint BytesPerBlock { get; } + /// + /// Gets the number of pixel rows per block. + /// + public uint RowsPerBlock { get; } + /// /// Gets the image width. /// diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs index b958982cec..5dfbdf148f 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs @@ -19,9 +19,10 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression /// The memory allocator. /// The bytes per row block. /// The bytes per row. + /// The pixel rows per block. /// The number of pixels per row. - protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) - : base(allocator, bytesPerBlock, bytesPerRow, width) + protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) { } diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs index 497bd492aa..0a7d4157f2 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs @@ -21,6 +21,7 @@ internal static class ExrCompressorFactory /// The output stream. /// The bytes per block. /// The bytes per row. + /// The pixel rows per block. /// The witdh of one row in pixels. /// The deflate compression level. /// A compressor for EXR image data. @@ -30,12 +31,13 @@ internal static class ExrCompressorFactory Stream output, uint bytesPerBlock, uint bytesPerRow, + uint rowsPerBlock, int width, DeflateCompressionLevel compressionLevel = DeflateCompressionLevel.DefaultCompression) => method switch { - ExrCompression.None => new NoneExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, width), - ExrCompression.Zips => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, width, compressionLevel), - ExrCompression.Zip => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, width, compressionLevel), + ExrCompression.None => new NoneExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.Zips => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, compressionLevel), + ExrCompression.Zip => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, compressionLevel), _ => throw ExrThrowHelper.NotSupportedCompressor(method.ToString()), }; } diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs index 6f63a932b7..353d1af117 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs @@ -22,6 +22,7 @@ internal static class ExrDecompressorFactory /// The bytes per row. /// The rows per block. /// The number of image channels. + /// The pixel type. /// Decompressor for EXR image data. public static ExrBaseDecompressor Create( ExrCompression method, @@ -33,10 +34,10 @@ internal static class ExrDecompressorFactory int channelCount, ExrPixelType pixelType) => method switch { - ExrCompression.None => new NoneExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, width), - ExrCompression.Zips => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, width), - ExrCompression.Zip => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, width), - ExrCompression.RunLengthEncoded => new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, width), + ExrCompression.None => new NoneExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.Zips => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.Zip => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), + ExrCompression.RunLengthEncoded => new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width), ExrCompression.B44 => new B44ExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount), ExrCompression.Pxr24 => new Pxr24Compression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount, pixelType), _ => throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)), diff --git a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs index 83887fd5f1..9e3fe8ec65 100644 --- a/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs +++ b/src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs @@ -14,9 +14,10 @@ internal abstract class ExrBaseCompressor : ExrBaseCompression /// The memory allocator. /// Bytes per row block. /// Bytes per pixel row. + /// The pixel rows per block. /// The number of pixels per row. - protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, int width) - : base(allocator, bytesPerBlock, bytesPerRow, width) + protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) + : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.Output = output; /// diff --git a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs index c6824c6aad..e2750d8972 100644 --- a/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrEncoderCore.cs @@ -181,7 +181,7 @@ internal sealed class ExrEncoderCore Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); Span alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width); - using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, width); + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, rowsPerBlock, width); ulong[] rowOffsets = new ulong[height]; for (uint y = 0; y < height; y += rowsPerBlock) @@ -273,7 +273,7 @@ internal sealed class ExrEncoderCore Span blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width); Span alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width); - using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, width); + using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, rowsPerBlock, width); Rgba128 rgb = default; ulong[] rowOffsets = new ulong[height]; From 7ad873aaf36211abc537ecca6ac7bae1f876d339 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 26 Apr 2026 15:39:18 +0200 Subject: [PATCH 11/13] Fix stylecop warnings --- .../Exr/Compression/Decompressors/RunLengthExrCompression.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs index bb3903deb8..f548a81810 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs @@ -14,14 +14,13 @@ internal class RunLengthExrCompression : ExrBaseDecompressor { private readonly IMemoryOwner tmpBuffer; - private readonly ushort[] s = new ushort[16]; - /// /// Initializes a new instance of the class. /// /// The memory allocator. /// The bytes per pixel row block. /// The bytes per row. + /// The pixel rows per block. /// The witdh of one row in pixels. public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width) : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); From 37f534ea5de4a80b854b3740ffb071ab1bfb47d8 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 1 May 2026 10:26:39 +1000 Subject: [PATCH 12/13] Add ImageFrame visitor; use Buffer2D Size/Bounds + expose subregion extensions --- .../Advanced/AdvancedImageExtensions.cs | 9 +++ src/ImageSharp/Advanced/IImageFrameVisitor.cs | 21 +++++++ src/ImageSharp/Advanced/IImageVisitor.cs | 4 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 2 +- src/ImageSharp/ImageFrame.cs | 13 ++++ src/ImageSharp/ImageFrame{TPixel}.cs | 15 ++--- src/ImageSharp/Image{TPixel}.cs | 1 - src/ImageSharp/Memory/Buffer2DExtensions.cs | 50 +++++----------- src/ImageSharp/Memory/Buffer2DRegion{T}.cs | 60 +++++++++---------- src/ImageSharp/Memory/Buffer2D{T}.cs | 21 +++++-- .../Transforms/Resize/ResizeWorker.cs | 2 +- .../Formats/Jpg/SpectralJpegTests.cs | 2 +- .../Memory/Buffer2DTests.SwapOrCopyContent.cs | 36 ++++++----- .../Memory/BufferAreaTests.cs | 8 +-- .../Processing/IntegralImageTests.cs | 2 +- .../TestUtilities/TestImageExtensions.cs | 2 +- 16 files changed, 139 insertions(+), 109 deletions(-) create mode 100644 src/ImageSharp/Advanced/IImageFrameVisitor.cs diff --git a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs index a451e111d2..e2c4258e2a 100644 --- a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs +++ b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs @@ -76,6 +76,15 @@ public static class AdvancedImageExtensions public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor, CancellationToken cancellationToken = default) => source.AcceptAsync(visitor, cancellationToken); + /// + /// Accepts a to implement a double-dispatch pattern in order to + /// apply pixel-specific operations on non-generic instances + /// + /// The source image frame. + /// The image visitor. + public static void AcceptVisitor(this ImageFrame source, IImageFrameVisitor visitor) + => source.Accept(visitor); + /// /// Gets the representation of the pixels as a containing the backing pixel data of the image /// stored in row major order, as a list of contiguous blocks in the source image's pixel format. diff --git a/src/ImageSharp/Advanced/IImageFrameVisitor.cs b/src/ImageSharp/Advanced/IImageFrameVisitor.cs new file mode 100644 index 0000000000..f2522110d5 --- /dev/null +++ b/src/ImageSharp/Advanced/IImageFrameVisitor.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Advanced; + +/// +/// A visitor to implement a double-dispatch pattern in order to apply pixel-specific operations +/// on non-generic instances. +/// +public interface IImageFrameVisitor +{ + /// + /// Provides a pixel-specific implementation for a given operation. + /// + /// The image frame. + /// The pixel type. + public void Visit(ImageFrame frame) + where TPixel : unmanaged, IPixel; +} diff --git a/src/ImageSharp/Advanced/IImageVisitor.cs b/src/ImageSharp/Advanced/IImageVisitor.cs index 5e8a4e4512..aee6909675 100644 --- a/src/ImageSharp/Advanced/IImageVisitor.cs +++ b/src/ImageSharp/Advanced/IImageVisitor.cs @@ -16,7 +16,7 @@ public interface IImageVisitor /// /// The image. /// The pixel type. - void Visit(Image image) + public void Visit(Image image) where TPixel : unmanaged, IPixel; } @@ -33,6 +33,6 @@ public interface IImageVisitorAsync /// The token to monitor for cancellation requests. /// The pixel type. /// A representing the asynchronous operation. - Task VisitAsync(Image image, CancellationToken cancellationToken) + public Task VisitAsync(Image image, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel; } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index d2883e2811..49640fae08 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -303,7 +303,7 @@ internal sealed class GifEncoderCore this.WriteGraphicalControlExtension(metadata, stream); Buffer2D indices = ((IPixelSource)quantized).PixelBuffer; - Rectangle interest = indices.FullRectangle(); + Rectangle interest = indices.Bounds; bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (metadata.ColorTableMode == FrameColorTableMode.Local); int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); diff --git a/src/ImageSharp/ImageFrame.cs b/src/ImageSharp/ImageFrame.cs index cd6fa6d98a..2d726dc4a0 100644 --- a/src/ImageSharp/ImageFrame.cs +++ b/src/ImageSharp/ImageFrame.cs @@ -71,6 +71,19 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable /// Whether to dispose of managed and unmanaged objects. protected abstract void Dispose(bool disposing); + /// + /// Accepts a . + /// Implemented by invoking + /// with the pixel type of the image. + /// + /// The visitor. + internal abstract void Accept(IImageFrameVisitor visitor); + + /// + /// Copies the pixel data of the image frame to a of a specific pixel type. + /// + /// The pixel type of the destination buffer. + /// The buffer to copy the pixel data to. internal abstract void CopyPixelsTo(Buffer2D destination) where TDestinationPixel : unmanaged, IPixel; diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index 4e25c4e581..9029d3eec2 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -339,6 +339,10 @@ public sealed class ImageFrame : ImageFrame, IPixelSource [MethodImpl(MethodImplOptions.AggressiveInlining)] internal ref TPixel GetPixelReference(int x, int y) => ref this.PixelBuffer[x, y]; + /// + internal override void Accept(IImageFrameVisitor visitor) + => visitor.Visit(this); + /// /// Copies the pixels to a of the same size. /// @@ -346,7 +350,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource /// ImageFrame{TPixel}.CopyTo(): target must be of the same size! internal void CopyTo(Buffer2D target) { - if (this.Size != target.Size()) + if (this.Size != target.Size) { throw new ArgumentException("ImageFrame.CopyTo(): target must be of the same size!", nameof(target)); } @@ -363,8 +367,8 @@ public sealed class ImageFrame : ImageFrame, IPixelSource { Guard.NotNull(source, nameof(source)); - Buffer2D.SwapOrCopyContent(this.PixelBuffer, source.PixelBuffer); - this.UpdateSize(this.PixelBuffer.Size()); + _ = Buffer2D.SwapOrCopyContent(this.PixelBuffer, source.PixelBuffer); + this.UpdateSize(this.PixelBuffer.Size); } /// @@ -475,10 +479,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource /// Clears the bitmap. /// /// The value to initialize the bitmap with. - internal void Clear(TPixel value) - { - this.PixelBuffer.Clear(value); - } + internal void Clear(TPixel value) => this.PixelBuffer.Clear(value); [MethodImpl(InliningOptions.ShortMethod)] private void VerifyCoords(int x, int y) diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index 18db343563..4881518c9c 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; diff --git a/src/ImageSharp/Memory/Buffer2DExtensions.cs b/src/ImageSharp/Memory/Buffer2DExtensions.cs index 96b9bc172f..290d978d0b 100644 --- a/src/ImageSharp/Memory/Buffer2DExtensions.cs +++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs @@ -104,60 +104,40 @@ public static class Buffer2DExtensions } /// - /// Returns a representing the full area of the buffer. - /// - /// The element type - /// The - /// The - internal static Rectangle FullRectangle(this Buffer2D buffer) - where T : struct - => new(0, 0, buffer.Width, buffer.Height); - - /// - /// Return a to the subregion represented by 'rectangle' + /// Return a to the subregion represented by . /// /// The element type /// The /// The rectangle subregion /// The - internal static Buffer2DRegion GetRegion(this Buffer2D buffer, Rectangle rectangle) + public static Buffer2DRegion GetRegion(this Buffer2D buffer, Rectangle rectangle) where T : unmanaged => new(buffer, rectangle); - internal static Buffer2DRegion GetRegion(this Buffer2D buffer, int x, int y, int width, int height) + /// + /// Return a to the specified area of . + /// + /// The element type. + /// The . + /// The X coordinate of the region. + /// The Y coordinate of the region. + /// The region width. + /// The region height. + /// The . + public static Buffer2DRegion GetRegion(this Buffer2D buffer, int x, int y, int width, int height) where T : unmanaged => new(buffer, new Rectangle(x, y, width, height)); /// - /// Return a to the whole area of 'buffer' + /// Return a to the whole area of . /// /// The element type /// The /// The - internal static Buffer2DRegion GetRegion(this Buffer2D buffer) + public static Buffer2DRegion GetRegion(this Buffer2D buffer) where T : unmanaged => new(buffer); - /// - /// Returns the size of the buffer. - /// - /// The element type - /// The - /// The of the buffer - internal static Size Size(this Buffer2D buffer) - where T : struct => - new(buffer.Width, buffer.Height); - - /// - /// Gets the bounds of the buffer. - /// - /// The element type - /// The - /// The - internal static Rectangle Bounds(this Buffer2D buffer) - where T : struct => - new(0, 0, buffer.Width, buffer.Height); - [Conditional("DEBUG")] private static void CheckColumnRegionsDoNotOverlap( Buffer2D buffer, diff --git a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs index 7bfb6f5731..c78c4ce9ee 100644 --- a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs +++ b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs @@ -15,17 +15,17 @@ public readonly struct Buffer2DRegion /// Initializes a new instance of the struct. /// /// The . - /// The defining a rectangular area within the buffer. + /// The defining a rectangular area within the buffer. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Buffer2DRegion(Buffer2D buffer, Rectangle rectangle) + public Buffer2DRegion(Buffer2D buffer, Rectangle bounds) { - DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.X, 0, nameof(rectangle)); - DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.Y, 0, nameof(rectangle)); - DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, buffer.Width, nameof(rectangle)); - DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, buffer.Height, nameof(rectangle)); + DebugGuard.MustBeGreaterThanOrEqualTo(bounds.X, 0, nameof(bounds)); + DebugGuard.MustBeGreaterThanOrEqualTo(bounds.Y, 0, nameof(bounds)); + DebugGuard.MustBeLessThanOrEqualTo(bounds.Width, buffer.Width, nameof(bounds)); + DebugGuard.MustBeLessThanOrEqualTo(bounds.Height, buffer.Height, nameof(bounds)); this.Buffer = buffer; - this.Rectangle = rectangle; + this.Bounds = bounds; } /// @@ -34,15 +34,10 @@ public readonly struct Buffer2DRegion /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public Buffer2DRegion(Buffer2D buffer) - : this(buffer, buffer.FullRectangle()) + : this(buffer, buffer.Bounds) { } - /// - /// Gets the rectangle specifying the boundaries of the area in . - /// - public Rectangle Rectangle { get; } - /// /// Gets the being pointed by this instance. /// @@ -51,12 +46,12 @@ public readonly struct Buffer2DRegion /// /// Gets the width /// - public int Width => this.Rectangle.Width; + public int Width => this.Bounds.Width; /// /// Gets the height /// - public int Height => this.Rectangle.Height; + public int Height => this.Bounds.Height; /// /// Gets the number of elements between row starts in . @@ -66,12 +61,17 @@ public readonly struct Buffer2DRegion /// /// Gets the size of the area. /// - internal Size Size => this.Rectangle.Size; + public Size Size => this.Bounds.Size; + + /// + /// Gets the rectangle specifying the boundaries of the area in . + /// + public Rectangle Bounds { get; } /// /// Gets a value indicating whether the area refers to the entire /// - internal bool IsFullBufferArea => this.Size == this.Buffer.Size(); + internal bool IsFullBufferArea => this.Size == this.Buffer.Size; /// /// Gets or sets a value at the given index. @@ -79,7 +79,7 @@ public readonly struct Buffer2DRegion /// The position inside a row /// The row index /// The reference to the value - internal ref T this[int x, int y] => ref this.Buffer[x + this.Rectangle.X, y + this.Rectangle.Y]; + internal ref T this[int x, int y] => ref this.Buffer[x + this.Bounds.X, y + this.Bounds.Y]; /// /// Gets a span to row 'y' inside this area. @@ -89,9 +89,9 @@ public readonly struct Buffer2DRegion [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span DangerousGetRowSpan(int y) { - int yy = this.Rectangle.Y + y; - int xx = this.Rectangle.X; - int width = this.Rectangle.Width; + int yy = this.Bounds.Y + y; + int xx = this.Bounds.X; + int width = this.Bounds.Width; return this.Buffer.DangerousGetRowSpan(yy).Slice(xx, width); } @@ -114,16 +114,16 @@ public readonly struct Buffer2DRegion /// /// Returns a subregion as . (Similar to .) /// - /// The specifying the boundaries of the subregion + /// The specifying the boundaries of the subregion /// The subregion [MethodImpl(MethodImplOptions.AggressiveInlining)] public Buffer2DRegion GetSubRegion(Rectangle rectangle) { - DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, this.Rectangle.Width, nameof(rectangle)); - DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, this.Rectangle.Height, nameof(rectangle)); + DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, this.Bounds.Width, nameof(rectangle)); + DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, this.Bounds.Height, nameof(rectangle)); - int x = this.Rectangle.X + rectangle.X; - int y = this.Rectangle.Y + rectangle.Y; + int x = this.Bounds.X + rectangle.X; + int y = this.Bounds.Y + rectangle.Y; rectangle = new Rectangle(x, y, rectangle.Width, rectangle.Height); return new Buffer2DRegion(this.Buffer, rectangle); } @@ -135,8 +135,8 @@ public readonly struct Buffer2DRegion [MethodImpl(MethodImplOptions.AggressiveInlining)] internal ref T GetReferenceToOrigin() { - int y = this.Rectangle.Y; - int x = this.Rectangle.X; + int y = this.Bounds.Y; + int x = this.Bounds.X; return ref this.Buffer.DangerousGetRowSpan(y)[x]; } @@ -152,7 +152,7 @@ public readonly struct Buffer2DRegion return; } - for (int y = 0; y < this.Rectangle.Height; y++) + for (int y = 0; y < this.Bounds.Height; y++) { Span row = this.DangerousGetRowSpan(y); row.Clear(); @@ -172,7 +172,7 @@ public readonly struct Buffer2DRegion return; } - for (int y = 0; y < this.Rectangle.Height; y++) + for (int y = 0; y < this.Bounds.Height; y++) { Span row = this.DangerousGetRowSpan(y); row.Fill(value); diff --git a/src/ImageSharp/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs index fe997a4fbf..cfd7664f01 100644 --- a/src/ImageSharp/Memory/Buffer2D{T}.cs +++ b/src/ImageSharp/Memory/Buffer2D{T}.cs @@ -39,20 +39,30 @@ public sealed class Buffer2D : IDisposable Guard.MustBeGreaterThanOrEqualTo(rowStride, width, nameof(rowStride)); this.FastMemoryGroup = memoryGroup; - this.Width = width; - this.Height = height; + this.Size = new Size(width, height); this.RowStride = rowStride; } /// /// Gets the width. /// - public int Width { get; private set; } + public int Width => this.Size.Width; /// /// Gets the height. /// - public int Height { get; private set; } + public int Height => this.Size.Height; + + /// + /// Gets the size of the buffer. + /// + public Size Size { get; private set; } + + /// + /// Gets the bounds of the buffer. + /// + /// The + public Rectangle Bounds => new(0, 0, this.Width, this.Height); /// /// Gets the number of elements between row starts in the backing memory. @@ -402,8 +412,7 @@ public sealed class Buffer2D : IDisposable source.CopyTo(destination); } - (destination.Width, source.Width) = (source.Width, destination.Width); - (destination.Height, source.Height) = (source.Height, destination.Height); + (destination.Size, source.Size) = (source.Size, destination.Size); (destination.RowStride, source.RowStride) = (source.RowStride, destination.RowStride); return swapped; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 0a4d386550..a0a8c2173e 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -59,7 +59,7 @@ internal sealed class ResizeWorker : IDisposable { this.configuration = configuration; this.source = source; - this.sourceRectangle = source.Rectangle; + this.sourceRectangle = source.Bounds; this.conversionModifiers = conversionModifiers; this.horizontalKernelMap = horizontalKernelMap; this.verticalKernelMap = verticalKernelMap; diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 903a6b0762..1610df6eb4 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -119,7 +119,7 @@ public class SpectralJpegTests this.Output.WriteLine($"Component{i}: [total: {total} | average: {average}]"); averageDifference += average; totalDifference += total; - Size s = libJpegComponent.SpectralBlocks.Size(); + Size s = libJpegComponent.SpectralBlocks.Size; tolerance += s.Width * s.Height; } diff --git a/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs b/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs index caf935cc47..6e9069a45e 100644 --- a/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs +++ b/tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Memory; @@ -15,26 +15,24 @@ public partial class Buffer2DTests [Fact] public void SwapOrCopyContent_WhenBothAllocated() { - using (Buffer2D a = this.memoryAllocator.Allocate2D(10, 5, AllocationOptions.Clean)) - using (Buffer2D b = this.memoryAllocator.Allocate2D(3, 7, AllocationOptions.Clean)) - { - a[1, 3] = 666; - b[1, 3] = 444; + using Buffer2D a = this.memoryAllocator.Allocate2D(10, 5, AllocationOptions.Clean); + using Buffer2D b = this.memoryAllocator.Allocate2D(3, 7, AllocationOptions.Clean); + a[1, 3] = 666; + b[1, 3] = 444; - Memory aa = a.FastMemoryGroup.Single(); - Memory bb = b.FastMemoryGroup.Single(); + Memory aa = a.FastMemoryGroup.Single(); + Memory bb = b.FastMemoryGroup.Single(); - Buffer2D.SwapOrCopyContent(a, b); + Buffer2D.SwapOrCopyContent(a, b); - Assert.Equal(bb, a.FastMemoryGroup.Single()); - Assert.Equal(aa, b.FastMemoryGroup.Single()); + Assert.Equal(bb, a.FastMemoryGroup.Single()); + Assert.Equal(aa, b.FastMemoryGroup.Single()); - Assert.Equal(new Size(3, 7), a.Size()); - Assert.Equal(new Size(10, 5), b.Size()); + Assert.Equal(new Size(3, 7), a.Size); + Assert.Equal(new Size(10, 5), b.Size); - Assert.Equal(666, b[1, 3]); - Assert.Equal(444, a[1, 3]); - } + Assert.Equal(666, b[1, 3]); + Assert.Equal(444, a[1, 3]); } [Fact] @@ -171,7 +169,7 @@ public partial class Buffer2DTests bool swap = Buffer2D.SwapOrCopyContent(dest, source); Assert.False(swap); - Assert.Equal(new Size(3, 2), dest.Size()); + Assert.Equal(new Size(3, 2), dest.Size); Assert.Equal(6, dest[2, 1]); } @@ -191,7 +189,7 @@ public partial class Buffer2DTests bool swap = Buffer2D.SwapOrCopyContent(dest, source); Assert.False(swap); - Assert.Equal(new Size(1, 5), dest.Size()); + Assert.Equal(new Size(1, 5), dest.Size); Assert.Equal(1, dest[0, 0]); Assert.Equal(2, dest[0, 1]); Assert.Equal(3, dest[0, 2]); @@ -215,7 +213,7 @@ public partial class Buffer2DTests bool swap = Buffer2D.SwapOrCopyContent(dest, source); Assert.False(swap); - Assert.Equal(new Size(2, 2), dest.Size()); + Assert.Equal(new Size(2, 2), dest.Size); Assert.Equal(1, dest[0, 0]); Assert.Equal(2, dest[1, 0]); Assert.Equal(3, dest[0, 1]); diff --git a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs index be7edbacca..a44d6ce1a1 100644 --- a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs +++ b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs @@ -17,7 +17,7 @@ public class BufferAreaTests Buffer2DRegion area = new(buffer, rectangle); Assert.Equal(buffer, area.Buffer); - Assert.Equal(rectangle, area.Rectangle); + Assert.Equal(rectangle, area.Bounds); } private Buffer2D CreateTestBuffer(int w, int h) @@ -90,7 +90,7 @@ public class BufferAreaTests Rectangle expectedRect = new(10, 12, 5, 5); Assert.Equal(buffer, area1.Buffer); - Assert.Equal(expectedRect, area1.Rectangle); + Assert.Equal(expectedRect, area1.Bounds); int value00 = (12 * 100) + 10; Assert.Equal(value00, area1[0, 0]); @@ -147,9 +147,9 @@ public class BufferAreaTests Assert.Equal(0, buffer[5, 5]); Assert.Equal(0, buffer[14, 14]); - for (int y = region.Rectangle.Y; y < region.Rectangle.Bottom; y++) + for (int y = region.Bounds.Y; y < region.Bounds.Bottom; y++) { - Span span = buffer.DangerousGetRowSpan(y).Slice(region.Rectangle.X, region.Width); + Span span = buffer.DangerousGetRowSpan(y).Slice(region.Bounds.X, region.Width); Assert.True(span.SequenceEqual(new int[region.Width])); } } diff --git a/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs b/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs index 81ba1693d2..a0ea99a9d0 100644 --- a/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs +++ b/tests/ImageSharp.Tests/Processing/IntegralImageTests.cs @@ -76,7 +76,7 @@ public class IntegralImageTests : BaseImageOperationsExtensionTest Buffer2D integralBuffer, Func getPixel) where TPixel : unmanaged, IPixel - => VerifySumValues(provider, integralBuffer, integralBuffer.Bounds(), getPixel); + => VerifySumValues(provider, integralBuffer, integralBuffer.Bounds, getPixel); private static void VerifySumValues( TestImageProvider provider, diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 4a34529dd3..be2356d657 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -505,7 +505,7 @@ public static class TestImageExtensions public static void CompareBuffers(Buffer2D expected, Buffer2D actual) where T : struct, IEquatable { - Assert.True(expected.Size() == actual.Size(), "Buffer sizes are not equal!"); + Assert.True(expected.Size == actual.Size, "Buffer sizes are not equal!"); for (int y = 0; y < expected.Height; y++) { From c1de5b61d3bcd96074dfa857b0d1f81f7ee10e95 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 2 May 2026 16:18:12 +0200 Subject: [PATCH 13/13] Fix mistake in inflateStream.AllocateNewBytes: Use compressedBytes instead of uncompressedBytes --- src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs index 5dfbdf148f..1b2d95ba53 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs @@ -52,7 +52,7 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression int left = (int)(compressedBytes - (stream.Position - pos)); return left > 0 ? left : 0; }); - inflateStream.AllocateNewBytes((int)uncompressedBytes, true); + inflateStream.AllocateNewBytes((int)compressedBytes, true); using DeflateStream dataStream = inflateStream.CompressedStream!; int totalRead = 0; @@ -69,7 +69,7 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression if (totalRead == 0) { - ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed image data!"); + ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed EXR image data!"); } return totalRead;