From 6ed2a275217d39301e76df42acec2a9533b39d2b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Sat, 5 Sep 2026 08:49:50 +1000 Subject: [PATCH] Handle partial EXR scanline blocks --- .../Decompressors/B44ExrCompression.cs | 2 +- .../Decompressors/NoneExrCompression.cs | 6 +++--- .../Decompressors/Pxr24Compression.cs | 11 ++++++----- .../Decompressors/RunLengthExrCompression.cs | 2 +- .../Decompressors/ZipExrCompression.cs | 9 ++++----- .../Exr/Compression/ExrBaseDecompressor.cs | 7 ++++--- src/ImageSharp/Formats/Exr/ExrDecoderCore.cs | 18 ++++++++++++++++-- .../Formats/Exr/ExrZipDecoderTests.cs | 2 +- 8 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs index 2b5b740569..e58eca4e98 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs @@ -39,7 +39,7 @@ internal class B44ExrCompression : ExrBaseDecompressor } /// - public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer) { Span outputBuffer = MemoryMarshal.Cast(buffer); Span decompressed = this.tmpBuffer.GetSpan(); diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs index 19edb31afe..b1a2094dab 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs @@ -25,10 +25,10 @@ internal class NoneExrCompression : ExrBaseDecompressor } /// - public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer) { - int bytesRead = stream.Read(buffer, 0, Math.Min(buffer.Length, (int)this.BytesPerBlock)); - if (bytesRead != (int)this.BytesPerBlock) + int bytesRead = stream.Read(buffer[..(int)uncompressedBytes]); + if (bytesRead != uncompressedBytes) { ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough pixel data from the stream!"); } diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs index f45b660e7d..d621c893e5 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs @@ -39,19 +39,20 @@ internal class Pxr24Compression : ExrBaseDecompressor } /// - public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer) { - Span uncompressed = this.tmpBuffer.GetSpan(); + uint rowCount = uncompressedBytes / this.BytesPerRow; + uint packedBytes = this.pixelType == ExrPixelType.Float ? (uncompressedBytes / 4) * 3 : uncompressedBytes; + Span uncompressed = this.tmpBuffer.GetSpan()[..(int)packedBytes]; Span outputBufferHalf = MemoryMarshal.Cast(buffer); Span outputBufferFloat = MemoryMarshal.Cast(buffer); Span outputBufferUint = MemoryMarshal.Cast(buffer); - uint uncompressedBytes = this.BytesPerBlock; - UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes); + UndoZipCompression(stream, compressedBytes, uncompressed, packedBytes); int lastIn = 0; int outputOffset = 0; - for (int y = 0; y < this.RowsPerBlock; y++) + for (uint y = 0; y < rowCount; 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 f548a81810..5ffe83af3c 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs @@ -26,7 +26,7 @@ internal class RunLengthExrCompression : ExrBaseDecompressor : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); /// - public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer) { Span uncompressed = this.tmpBuffer.GetSpan(); int maxLength = (int)this.BytesPerBlock; diff --git a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs index 8bab76f402..41fda54366 100644 --- a/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs +++ b/src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs @@ -26,15 +26,14 @@ internal class ZipExrCompression : ExrBaseDecompressor : base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate((int)bytesPerBlock); /// - public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer) + public override void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer) { - Span uncompressed = this.tmpBuffer.GetSpan(); + Span uncompressed = this.tmpBuffer.GetSpan()[..(int)uncompressedBytes]; - uint uncompressedBytes = (uint)buffer.Length; int totalRead = UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes); - Reconstruct(uncompressed, (uint)totalRead); - Interleave(uncompressed, (uint)totalRead, buffer); + Reconstruct(uncompressed, uncompressedBytes); + Interleave(uncompressed, uncompressedBytes, buffer); } /// diff --git a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs index 2f1320e587..52a8164c7a 100644 --- a/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs +++ b/src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs @@ -31,8 +31,9 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression /// /// The buffered stream to decompress. /// The compressed bytes. + /// The expected byte count for the current block. /// The buffer to write the decompressed data to. - public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span buffer); + public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, uint uncompressedBytes, Span buffer); /// /// Decompresses zip compressed data. @@ -67,9 +68,9 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression totalRead += bytesRead; } - if (totalRead != uncompressedBytes) + if (totalRead != uncompressedBytes || dataStream.ReadByte() != -1) { - ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed EXR image data!"); + ExrThrowHelper.ThrowInvalidImageContentException("ZIP compressed EXR block has an invalid decompressed length."); } return totalRead; diff --git a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs index 680e8d333d..9dd333d92f 100644 --- a/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs +++ b/src/ImageSharp/Formats/Exr/ExrDecoderCore.cs @@ -193,9 +193,16 @@ internal sealed class ExrDecoderCore : ImageDecoderCore this.ValidateChunkOffset(rowOffset, stream); stream.Position = (long)rowOffset; uint rowStartIndex = this.ReadUnsignedInteger(stream); + if (rowStartIndex >= height) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk row index is outside the data window."); + } uint compressedBytesCount = this.ReadUnsignedInteger(stream); - decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); + uint rowsInBlock = Math.Min(rowsPerBlock, (uint)height - rowStartIndex); + uint uncompressedBytesCount = (uint)(bytesPerRow * rowsInBlock); + + decompressor.Decompress(stream, compressedBytesCount, uncompressedBytesCount, decompressedPixelData); int offset = 0; for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) @@ -274,9 +281,16 @@ internal sealed class ExrDecoderCore : ImageDecoderCore this.ValidateChunkOffset(rowOffset, stream); stream.Position = (long)rowOffset; uint rowStartIndex = this.ReadUnsignedInteger(stream); + if (rowStartIndex >= height) + { + ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk row index is outside the data window."); + } uint compressedBytesCount = this.ReadUnsignedInteger(stream); - decompressor.Decompress(stream, compressedBytesCount, decompressedPixelData); + uint rowsInBlock = Math.Min(rowsPerBlock, (uint)height - rowStartIndex); + uint uncompressedBytesCount = (uint)(bytesPerRow * rowsInBlock); + + decompressor.Decompress(stream, compressedBytesCount, uncompressedBytesCount, decompressedPixelData); int offset = 0; for (uint rowIndex = rowStartIndex; rowIndex < rowStartIndex + rowsPerBlock && rowIndex < height; rowIndex++) diff --git a/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs index afce3257fc..23d9ccedd4 100644 --- a/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Exr/ExrZipDecoderTests.cs @@ -33,7 +33,7 @@ public class ExrZipDecoderTests private static byte[] BuildExr(byte[] compressed) { - const int width = 64; + const int width = 256; const int height = 1; using MemoryStream output = new();