Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 92 additions & 25 deletions src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ public ExrDecoderCore(ExrDecoderOptions options)
/// </summary>
private ExrHeaderAttributes HeaderAttributes { get; set; }

/// <summary>
/// Gets or sets the earliest valid stream position for a scanline chunk.
/// </summary>
private long MinimumChunkOffset { get; set; }

/// <inheritdoc />
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
{
Expand All @@ -100,24 +105,33 @@ protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, Cance
ExrThrowHelper.ThrowNotSupported($"Compression {this.Compression} is not yet supported");
}

Image<TPixel> image = new(this.configuration, this.Width, this.Height, this.metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
Image<TPixel> image = null;
try
{
image = new Image<TPixel>(this.configuration, this.Width, this.Height, this.metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();

switch (this.PixelType)
{
case ExrPixelType.Half:
case ExrPixelType.Float:
this.DecodeFloatingPointPixelData(stream, pixels, cancellationToken);
break;
case ExrPixelType.UnsignedInt:
this.DecodeUnsignedIntPixelData(stream, pixels, cancellationToken);
break;
default:
ExrThrowHelper.ThrowNotSupported("Pixel type is not supported");
break;
}

switch (this.PixelType)
return image;
}
catch
{
case ExrPixelType.Half:
case ExrPixelType.Float:
this.DecodeFloatingPointPixelData(stream, pixels, cancellationToken);
break;
case ExrPixelType.UnsignedInt:
this.DecodeUnsignedIntPixelData(stream, pixels, cancellationToken);
break;
default:
ExrThrowHelper.ThrowNotSupported("Pixel type is not supported");
break;
image?.Dispose();
throw;
}

return image;
}

/// <inheritdoc />
Expand All @@ -139,9 +153,14 @@ private void DecodeFloatingPointPixelData<TPixel>(BufferedReadStream stream, Buf
where TPixel : unmanaged, IPixel<TPixel>
{
bool hasAlpha = this.HasAlpha();
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
if (bytesPerBlock > int.MaxValue)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR block size exceeds the maximum allowed size.");
}

int width = this.Width;
int height = this.Height;
int channelCount = this.Channels.Count;
Expand All @@ -158,8 +177,8 @@ private void DecodeFloatingPointPixelData<TPixel>(BufferedReadStream stream, Buf
this.Compression,
this.memoryAllocator,
width,
bytesPerBlock,
bytesPerRow,
(uint)bytesPerBlock,
(uint)bytesPerRow,
rowsPerBlock,
channelCount,
this.PixelType);
Expand All @@ -170,6 +189,7 @@ private void DecodeFloatingPointPixelData<TPixel>(BufferedReadStream stream, Buf
ulong rowOffset = this.ReadUnsignedLong(stream);
long nextRowOffsetPosition = stream.Position;

this.ValidateChunkOffset(rowOffset, stream);
stream.Position = (long)rowOffset;
uint rowStartIndex = this.ReadUnsignedInteger(stream);

Expand Down Expand Up @@ -212,9 +232,14 @@ private void DecodeUnsignedIntPixelData<TPixel>(BufferedReadStream stream, Buffe
where TPixel : unmanaged, IPixel<TPixel>
{
bool hasAlpha = this.HasAlpha();
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(this.Channels, (uint)this.Width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
if (bytesPerBlock > int.MaxValue)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR block size exceeds the maximum allowed size.");
}

int width = this.Width;
int height = this.Height;
int channelCount = this.Channels.Count;
Expand All @@ -231,8 +256,8 @@ private void DecodeUnsignedIntPixelData<TPixel>(BufferedReadStream stream, Buffe
this.Compression,
this.memoryAllocator,
width,
bytesPerBlock,
bytesPerRow,
(uint)bytesPerBlock,
(uint)bytesPerRow,
rowsPerBlock,
channelCount,
this.PixelType);
Expand All @@ -243,6 +268,7 @@ private void DecodeUnsignedIntPixelData<TPixel>(BufferedReadStream stream, Buffe
ulong rowOffset = this.ReadUnsignedLong(stream);
long nextRowOffsetPosition = stream.Position;

this.ValidateChunkOffset(rowOffset, stream);
stream.Position = (long)rowOffset;
uint rowStartIndex = this.ReadUnsignedInteger(stream);

Expand Down Expand Up @@ -597,10 +623,38 @@ private ExrHeaderAttributes ReadExrHeader(BufferedReadStream stream)

this.HeaderAttributes = this.ParseHeaderAttributes(stream);

this.Width = this.HeaderAttributes.DataWindow.XMax - this.HeaderAttributes.DataWindow.XMin + 1;
this.Height = this.HeaderAttributes.DataWindow.YMax - this.HeaderAttributes.DataWindow.YMin + 1;
ExrBox2i dataWindow = this.HeaderAttributes.DataWindow;
if (dataWindow.XMax < dataWindow.XMin || dataWindow.YMax < dataWindow.YMin)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR DataWindow max values must be greater than or equal to min values.");
}

long width = (long)dataWindow.XMax - dataWindow.XMin + 1;
long height = (long)dataWindow.YMax - dataWindow.YMin + 1;

// Decoding stages each row as four color planes, so the width must be bounded
// before later width * 4 buffer sizing can overflow.
if (width > int.MaxValue / 4 || height > int.MaxValue)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR DataWindow dimensions exceed the maximum allowed size.");
}

this.Width = (int)width;
this.Height = (int)height;
Comment on lines +632 to +643
this.Channels = this.HeaderAttributes.Channels;
this.Compression = this.HeaderAttributes.Compression;
uint rowsPerBlock = ExrUtils.RowsPerBlock(this.Compression);
long chunkCount = (this.Height + (long)rowsPerBlock - 1) / rowsPerBlock;
long offsetTableByteCount = chunkCount * sizeof(ulong);

// The scanline offset table sits between the header and pixel chunks; proving it
// fits in the stream keeps all later chunk offsets on the pixel-data side.
if (stream.Position > stream.Length || offsetTableByteCount > stream.Length - stream.Position)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk offset table is outside the bounds of the stream.");
}

this.MinimumChunkOffset = stream.Position + offsetTableByteCount;
this.PixelType = this.ValidateChannels();
this.ImageDataType = this.DetermineImageDataType();

Expand Down Expand Up @@ -866,6 +920,19 @@ private static string ReadString(BufferedReadStream stream)
_ => false,
};

/// <summary>
/// Validates a scanline chunk offset read from the EXR offset table.
/// </summary>
/// <param name="chunkOffset">The chunk offset to validate.</param>
/// <param name="stream">The stream containing the image data.</param>
private void ValidateChunkOffset(ulong chunkOffset, BufferedReadStream stream)
{
if (chunkOffset < (ulong)this.MinimumChunkOffset || chunkOffset >= (ulong)stream.Length)
{
ExrThrowHelper.ThrowInvalidImageContentException("EXR chunk offset is outside the bounds of the stream.");
}
}

/// <summary>
/// Determines whether this image has alpha channel.
/// </summary>
Expand Down
20 changes: 14 additions & 6 deletions src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,13 @@ private ulong[] EncodeFloatingPointPixelData<TPixel>(
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue)
{
throw new ImageFormatException("Image is too large to encode in EXR format.");
}

using IMemoryOwner<float> rgbBuffer = this.memoryAllocator.Allocate<float>(width * 4, AllocationOptions.Clean);
using IMemoryOwner<byte> rowBlockBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock, AllocationOptions.Clean);
Expand All @@ -181,7 +185,7 @@ private ulong[] EncodeFloatingPointPixelData<TPixel>(
Span<float> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
Span<float> alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width);

using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, rowsPerBlock, width);
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width);

ulong[] rowOffsets = new ulong[height];
for (uint y = 0; y < height; y += rowsPerBlock)
Expand Down Expand Up @@ -262,9 +266,13 @@ private ulong[] EncodeUnsignedIntPixelData<TPixel>(
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
uint bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
ulong bytesPerRow = ExrUtils.CalculateBytesPerRow(channels, (uint)width);
uint rowsPerBlock = ExrUtils.RowsPerBlock(compression);
uint bytesPerBlock = bytesPerRow * rowsPerBlock;
ulong bytesPerBlock = bytesPerRow * rowsPerBlock;
if (bytesPerRow > uint.MaxValue || bytesPerBlock > int.MaxValue)
{
throw new ImageFormatException("Image is too large to encode in EXR format.");
}

using IMemoryOwner<uint> rgbBuffer = this.memoryAllocator.Allocate<uint>(width * 4, AllocationOptions.Clean);
using IMemoryOwner<byte> rowBlockBuffer = this.memoryAllocator.Allocate<byte>((int)bytesPerBlock, AllocationOptions.Clean);
Expand All @@ -273,7 +281,7 @@ private ulong[] EncodeUnsignedIntPixelData<TPixel>(
Span<uint> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
Span<uint> alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width);

using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, rowsPerBlock, width);
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, (uint)bytesPerBlock, (uint)bytesPerRow, rowsPerBlock, width);

Rgba128 rgb = default;
ulong[] rowOffsets = new ulong[height];
Expand Down
8 changes: 4 additions & 4 deletions src/ImageSharp/Formats/Exr/ExrUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ internal static class ExrUtils
/// <param name="channels">The image channels array.</param>
/// <param name="width">The width in pixels of a row.</param>
/// <returns>The number of bytes per row.</returns>
public static uint CalculateBytesPerRow(IList<ExrChannelInfo> channels, uint width)
public static ulong CalculateBytesPerRow(IList<ExrChannelInfo> channels, uint width)
{
uint bytesPerRow = 0;
ulong bytesPerRow = 0;
foreach (ExrChannelInfo channelInfo in channels)
{
if (channelInfo.ChannelName.Equals("A", StringComparison.Ordinal)
Expand All @@ -26,11 +26,11 @@ public static uint CalculateBytesPerRow(IList<ExrChannelInfo> channels, uint wid
{
if (channelInfo.PixelType == ExrPixelType.Half)
{
bytesPerRow += 2 * width;
bytesPerRow += 2UL * width;
}
else
{
bytesPerRow += 4 * width;
bytesPerRow += 4UL * width;
}
}
}
Expand Down
Loading
Loading