diff --git a/.gitattributes b/.gitattributes
index 355b64dce1..f97695f901 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -115,6 +115,7 @@
*.jpg filter=lfs diff=lfs merge=lfs -text
*.jpeg filter=lfs diff=lfs merge=lfs -text
*.bmp filter=lfs diff=lfs merge=lfs -text
+*.BMP filter=lfs diff=lfs merge=lfs -text
*.gif filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text
*.tif filter=lfs diff=lfs merge=lfs -text
diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index b2179fbce5..45769c2163 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -22,7 +22,7 @@ jobs:
sdk-preview: true
runtime: -x64
codecov: false
- - os: macos-latest
+ - os: macos-13 # macos-latest runs on arm64 runners
framework: net6.0
sdk: 6.0.x
sdk-preview: true
@@ -38,7 +38,7 @@ jobs:
framework: net5.0
runtime: -x64
codecov: false
- - os: macos-latest
+ - os: macos-13 # macos-latest runs on arm64 runners
framework: net5.0
runtime: -x64
codecov: false
@@ -50,7 +50,7 @@ jobs:
framework: netcoreapp3.1
runtime: -x64
codecov: false
- - os: macos-latest
+ - os: macos-13 # macos-latest runs on arm64 runners
framework: netcoreapp3.1
runtime: -x64
codecov: false
@@ -74,6 +74,16 @@ jobs:
runs-on: ${{matrix.options.os}}
steps:
+ - name: Install Ubuntu prerequisites
+ if: ${{ contains(matrix.options.os, 'ubuntu') }}
+ run: |
+ # libssl 1.1 (required by old .NET runtimes)
+ wget http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.0g-2ubuntu4_amd64.deb
+ sudo dpkg -i libssl1.1_1.1.0g-2ubuntu4_amd64.deb
+
+ # libgdiplus
+ sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev
+
- name: Git Config
shell: bash
run: |
@@ -91,7 +101,7 @@ jobs:
run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
- name: Git Setup LFS Cache
- uses: actions/cache@v2
+ uses: actions/cache@v4
id: lfs-cache
with:
path: .git/lfs
@@ -104,7 +114,7 @@ jobs:
uses: NuGet/setup-nuget@v1
- name: NuGet Setup Cache
- uses: actions/cache@v2
+ uses: actions/cache@v4
id: nuget-cache
with:
path: ~/.nuget
@@ -151,7 +161,7 @@ jobs:
XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit
- name: Export Failed Output
- uses: actions/upload-artifact@v2
+ uses: actions/upload-artifact@v4
if: failure()
with:
name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip
@@ -181,7 +191,7 @@ jobs:
uses: NuGet/setup-nuget@v1
- name: NuGet Setup Cache
- uses: actions/cache@v2
+ uses: actions/cache@v4
id: nuget-cache
with:
path: ~/.nuget
@@ -192,9 +202,15 @@ jobs:
shell: pwsh
run: ./ci-pack.ps1
- - name: MyGet Publish
+ - name: Feedz Publish
shell: pwsh
run: |
- dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v2/package
- dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v3/index.json
- # TODO: If github.ref starts with 'refs/tags' then it was tag push and we can optionally push out package to nuget.org
+ dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.FEEDZ_TOKEN}} -s https://f.feedz.io/sixlabors/sixlabors/nuget/index.json --skip-duplicate
+ dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.FEEDZ_TOKEN}} -s https://f.feedz.io/sixlabors/sixlabors/symbols --skip-duplicate
+ - name: NuGet Publish
+ if: ${{ startsWith(github.ref, 'refs/tags/') }}
+ shell: pwsh
+ run: |
+ dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate
+ dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.NUGET_TOKEN}} -s https://api.nuget.org/v3/index.json --skip-duplicate
+
diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml
index 2b14f2a4b7..45b856a15c 100644
--- a/.github/workflows/code-coverage.yml
+++ b/.github/workflows/code-coverage.yml
@@ -34,7 +34,7 @@ jobs:
run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id
- name: Git Setup LFS Cache
- uses: actions/cache@v2
+ uses: actions/cache@v4
id: lfs-cache
with:
path: .git/lfs
@@ -47,7 +47,7 @@ jobs:
uses: NuGet/setup-nuget@v1
- name: NuGet Setup Cache
- uses: actions/cache@v2
+ uses: actions/cache@v4
id: nuget-cache
with:
path: ~/.nuget
diff --git a/Directory.Build.props b/Directory.Build.props
index 26b3cc5afc..524cf27890 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -23,7 +23,7 @@
- preview
+ 10.0
+
+
+ https://api.nuget.org/v3/index.json;
+ https://f.feedz.io/sixlabors/sixlabors/nuget/index.json;
+ https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json;
+
+
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index faa29865f2..904d404f50 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -18,7 +18,7 @@
- true
+
diff --git a/src/ImageSharp/Advanced/ParallelRowIterator.cs b/src/ImageSharp/Advanced/ParallelRowIterator.cs
index e787b7cfc5..06fbe731d1 100644
--- a/src/ImageSharp/Advanced/ParallelRowIterator.cs
+++ b/src/ImageSharp/Advanced/ParallelRowIterator.cs
@@ -52,7 +52,7 @@ public static void IterateRows(
int width = rectangle.Width;
int height = rectangle.Height;
- int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
+ int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
// Avoid TPL overhead in this trivial case:
@@ -117,7 +117,7 @@ public static void IterateRows(
int width = rectangle.Width;
int height = rectangle.Height;
- int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
+ int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
@@ -181,7 +181,7 @@ public static void IterateRowIntervals(
int width = rectangle.Width;
int height = rectangle.Height;
- int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
+ int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
// Avoid TPL overhead in this trivial case:
@@ -243,7 +243,7 @@ public static void IterateRowIntervals(
int width = rectangle.Width;
int height = rectangle.Height;
- int maxSteps = DivideCeil(width * height, parallelSettings.MinimumPixelsProcessedPerTask);
+ int maxSteps = DivideCeil(width * (long)height, parallelSettings.MinimumPixelsProcessedPerTask);
int numOfSteps = Math.Min(parallelSettings.MaxDegreeOfParallelism, maxSteps);
MemoryAllocator allocator = parallelSettings.MemoryAllocator;
@@ -270,7 +270,7 @@ public static void IterateRowIntervals(
}
[MethodImpl(InliningOptions.ShortMethod)]
- private static int DivideCeil(int dividend, int divisor) => 1 + ((dividend - 1) / divisor);
+ private static int DivideCeil(long dividend, int divisor) => (int)Math.Min(1 + ((dividend - 1) / divisor), int.MaxValue);
private static void ValidateRectangle(Rectangle rectangle)
{
diff --git a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
index f4b0543b84..bb97ff79eb 100644
--- a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
+++ b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
@@ -161,6 +161,11 @@ public override int Read(byte[] buffer, int offset, int count)
bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining);
this.currentDataRemaining -= bytesToRead;
bytesRead = this.innerStream.Read(buffer, offset, bytesToRead);
+ if (bytesRead == 0)
+ {
+ return totalBytesRead;
+ }
+
totalBytesRead += bytesRead;
}
@@ -168,22 +173,13 @@ public override int Read(byte[] buffer, int offset, int count)
}
///
- public override long Seek(long offset, SeekOrigin origin)
- {
- throw new NotSupportedException();
- }
+ public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
///
- public override void SetLength(long value)
- {
- throw new NotSupportedException();
- }
+ public override void SetLength(long value) => throw new NotSupportedException();
///
- public override void Write(byte[] buffer, int offset, int count)
- {
- throw new NotSupportedException();
- }
+ public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
///
protected override void Dispose(bool disposing)
@@ -245,22 +241,17 @@ private bool InitializeInflateStream(bool isCriticalChunk)
// CINFO is not defined in this specification for CM not equal to 8.
throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}");
}
- else
- {
- return false;
- }
+
+ return false;
}
}
+ else if (isCriticalChunk)
+ {
+ throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}");
+ }
else
{
- if (isCriticalChunk)
- {
- throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}");
- }
- else
- {
- return false;
- }
+ return false;
}
// The preset dictionary.
@@ -269,7 +260,11 @@ private bool InitializeInflateStream(bool isCriticalChunk)
{
// We don't need this for inflate so simply skip by the next four bytes.
// https://tools.ietf.org/html/rfc1950#page-6
- this.innerStream.Read(ChecksumBuffer, 0, 4);
+ if (this.innerStream.Read(ChecksumBuffer, 0, 4) != 4)
+ {
+ return false;
+ }
+
this.currentDataRemaining -= 4;
}
diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs
index 6d6cfc0792..d0682e5413 100644
--- a/src/ImageSharp/Formats/Gif/GifDecoder.cs
+++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs
@@ -23,6 +23,11 @@ public sealed class GifDecoder : IImageDecoder, IGifDecoderOptions, IImageInfoDe
///
public FrameDecodingMode DecodingMode { get; set; } = FrameDecodingMode.All;
+ ///
+ /// Gets or sets the maximum number of gif frames.
+ ///
+ public uint MaxFrames { get; set; } = uint.MaxValue;
+
///
public Image Decode(Configuration configuration, Stream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
index d17e89cd45..886f667e4c 100644
--- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
+++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs
@@ -3,6 +3,7 @@
using System;
using System.Buffers;
+using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -22,19 +23,24 @@ namespace SixLabors.ImageSharp.Formats.Gif
internal sealed class GifDecoderCore : IImageDecoderInternals
{
///
- /// The temp buffer used to reduce allocations.
+ /// The temp buffer.
///
- private readonly byte[] buffer = new byte[16];
+ private byte[] buffer = new byte[16];
///
- /// The currently loaded stream.
+ /// The global color table.
///
- private BufferedReadStream stream;
+ private IMemoryOwner globalColorTable;
///
- /// The global color table.
+ /// The current local color table.
///
- private IMemoryOwner globalColorTable;
+ private IMemoryOwner currentLocalColorTable;
+
+ ///
+ /// Gets the size in bytes of the current local color table.
+ ///
+ private int currentLocalColorTableSize;
///
/// The area to restore.
@@ -56,6 +62,26 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
///
private GifImageDescriptor imageDescriptor;
+ ///
+ /// The global configuration.
+ ///
+ private readonly Configuration configuration;
+
+ ///
+ /// Used for allocating memory during processing operations.
+ ///
+ private readonly MemoryAllocator memoryAllocator;
+
+ ///
+ /// The maximum number of frames to decode. Inclusive.
+ ///
+ private readonly uint maxFrames;
+
+ ///
+ /// Whether to skip metadata during decode.
+ ///
+ private readonly bool skipMetadata;
+
///
/// The abstract metadata.
///
@@ -73,23 +99,14 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// The decoder options.
public GifDecoderCore(Configuration configuration, IGifDecoderOptions options)
{
- this.IgnoreMetadata = options.IgnoreMetadata;
- this.DecodingMode = options.DecodingMode;
- this.Configuration = configuration ?? Configuration.Default;
+ this.skipMetadata = options.IgnoreMetadata;
+ this.configuration = configuration ?? Configuration.Default;
+ this.maxFrames = options.DecodingMode == FrameDecodingMode.All ? options.MaxFrames : 1;
+ this.memoryAllocator = this.configuration.MemoryAllocator;
}
///
- public Configuration Configuration { get; }
-
- ///
- /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
- ///
- public bool IgnoreMetadata { get; internal set; }
-
- ///
- /// Gets the decoding mode for multi-frame images.
- ///
- public FrameDecodingMode DecodingMode { get; }
+ public Configuration Configuration => this.configuration;
///
/// Gets the dimensions of the image.
@@ -102,6 +119,7 @@ public GifDecoderCore(Configuration configuration, IGifDecoderOptions options)
public Image Decode(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
+ uint frameCount = 0;
Image image = null;
ImageFrame previousFrame = null;
try
@@ -114,28 +132,32 @@ public Image Decode(BufferedReadStream stream, CancellationToken
{
if (nextFlag == GifConstants.ImageLabel)
{
- if (previousFrame != null && this.DecodingMode == FrameDecodingMode.First)
+ if (previousFrame != null && ++frameCount == this.maxFrames)
{
break;
}
- this.ReadFrame(ref image, ref previousFrame);
+ this.ReadFrame(stream, ref image, ref previousFrame);
+
+ // Reset per-frame state.
+ this.imageDescriptor = default;
+ this.graphicsControlExtension = default;
}
else if (nextFlag == GifConstants.ExtensionIntroducer)
{
switch (stream.ReadByte())
{
case GifConstants.GraphicControlLabel:
- this.ReadGraphicalControlExtension();
+ this.ReadGraphicalControlExtension(stream);
break;
case GifConstants.CommentLabel:
- this.ReadComments();
+ this.ReadComments(stream);
break;
case GifConstants.ApplicationExtensionLabel:
- this.ReadApplicationExtension();
+ this.ReadApplicationExtension(stream);
break;
case GifConstants.PlainTextLabel:
- this.SkipBlock(); // Not supported by any known decoder.
+ SkipBlock(stream); // Not supported by any known decoder.
break;
}
}
@@ -154,6 +176,12 @@ public Image Decode(BufferedReadStream stream, CancellationToken
finally
{
this.globalColorTable?.Dispose();
+ this.currentLocalColorTable?.Dispose();
+ }
+
+ if (image is null)
+ {
+ GifThrowHelper.ThrowInvalidImageContentException("No data");
}
return image;
@@ -162,6 +190,9 @@ public Image Decode(BufferedReadStream stream, CancellationToken
///
public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken)
{
+ uint frameCount = 0;
+ ImageFrameMetadata? previousFrame = null;
+ List framesMetadata = new();
try
{
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
@@ -172,23 +203,32 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
{
if (nextFlag == GifConstants.ImageLabel)
{
- this.ReadImageDescriptor();
+ if (previousFrame != null && ++frameCount == this.maxFrames)
+ {
+ break;
+ }
+
+ this.ReadFrameMetadata(stream, framesMetadata, ref previousFrame);
+
+ // Reset per-frame state.
+ this.imageDescriptor = default;
+ this.graphicsControlExtension = default;
}
else if (nextFlag == GifConstants.ExtensionIntroducer)
{
switch (stream.ReadByte())
{
case GifConstants.GraphicControlLabel:
- this.SkipBlock(); // Skip graphic control extension block
+ this.ReadGraphicalControlExtension(stream);
break;
case GifConstants.CommentLabel:
- this.ReadComments();
+ this.ReadComments(stream);
break;
case GifConstants.ApplicationExtensionLabel:
- this.ReadApplicationExtension();
+ this.ReadApplicationExtension(stream);
break;
case GifConstants.PlainTextLabel:
- this.SkipBlock(); // Not supported by any known decoder.
+ SkipBlock(stream); // Not supported by any known decoder.
break;
}
}
@@ -207,6 +247,12 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
finally
{
this.globalColorTable?.Dispose();
+ this.currentLocalColorTable?.Dispose();
+ }
+
+ if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0)
+ {
+ GifThrowHelper.ThrowNoHeader();
}
return new ImageInfo(
@@ -219,9 +265,10 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
///
/// Reads the graphic control extension.
///
- private void ReadGraphicalControlExtension()
+ /// The containing image data.
+ private void ReadGraphicalControlExtension(BufferedReadStream stream)
{
- int bytesRead = this.stream.Read(this.buffer, 0, 6);
+ int bytesRead = stream.Read(this.buffer, 0, 6);
if (bytesRead != 6)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension");
@@ -233,9 +280,10 @@ private void ReadGraphicalControlExtension()
///
/// Reads the image descriptor.
///
- private void ReadImageDescriptor()
+ /// The containing image data.
+ private void ReadImageDescriptor(BufferedReadStream stream)
{
- int bytesRead = this.stream.Read(this.buffer, 0, 9);
+ int bytesRead = stream.Read(this.buffer, 0, 9);
if (bytesRead != 9)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor");
@@ -251,9 +299,10 @@ private void ReadImageDescriptor()
///
/// Reads the logical screen descriptor.
///
- private void ReadLogicalScreenDescriptor()
+ /// The containing image data.
+ private void ReadLogicalScreenDescriptor(BufferedReadStream stream)
{
- int bytesRead = this.stream.Read(this.buffer, 0, 7);
+ int bytesRead = stream.Read(this.buffer, 0, 7);
if (bytesRead != 7)
{
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor");
@@ -266,103 +315,115 @@ private void ReadLogicalScreenDescriptor()
/// Reads the application extension block parsing any animation or XMP information
/// if present.
///
- private void ReadApplicationExtension()
+ /// The containing image data.
+ private void ReadApplicationExtension(BufferedReadStream stream)
{
- int appLength = this.stream.ReadByte();
+ int appLength = stream.ReadByte();
// If the length is 11 then it's a valid extension and most likely
// a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this.
+ long position = stream.Position;
if (appLength == GifConstants.ApplicationBlockSize)
{
- this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
+ stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes);
-
- if (isXmp && !this.IgnoreMetadata)
+ if (isXmp && !this.skipMetadata)
{
- var extension = GifXmpApplicationExtension.Read(this.stream, this.MemoryAllocator);
+ GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator);
if (extension.Data.Length > 0)
{
- this.metadata.XmpProfile = new XmpProfile(extension.Data);
+ this.metadata!.XmpProfile = new XmpProfile(extension.Data);
+ }
+ else
+ {
+ // Reset the stream position and continue.
+ stream.Position = position;
+ SkipBlock(stream, appLength);
}
return;
}
- else
- {
- int subBlockSize = this.stream.ReadByte();
- // TODO: There's also a NETSCAPE buffer extension.
- // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
- if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
- {
- this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
- this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount;
- this.stream.Skip(1); // Skip the terminator.
- return;
- }
+ int subBlockSize = stream.ReadByte();
- // Could be something else not supported yet.
- // Skip the subblock and terminator.
- this.SkipBlock(subBlockSize);
+ // TODO: There's also a NETSCAPE buffer extension.
+ // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
+ if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
+ {
+ stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
+ this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount;
+ stream.Skip(1); // Skip the terminator.
+ return;
}
+ // Could be something else not supported yet.
+ // Skip the subblock and terminator.
+ SkipBlock(stream, subBlockSize);
+
return;
}
- this.SkipBlock(appLength); // Not supported by any known decoder.
+ SkipBlock(stream, appLength); // Not supported by any known decoder.
}
///
/// Skips over a block or reads its terminator.
- /// The length of the block to skip.
///
- private void SkipBlock(int blockSize = 0)
+ /// The containing image data.
+ /// The length of the block to skip.
+ private static void SkipBlock(BufferedReadStream stream, int blockSize = 0)
{
if (blockSize > 0)
{
- this.stream.Skip(blockSize);
+ stream.Skip(blockSize);
}
int flag;
- while ((flag = this.stream.ReadByte()) > 0)
+ while ((flag = stream.ReadByte()) > 0)
{
- this.stream.Skip(flag);
+ stream.Skip(flag);
}
}
///
/// Reads the gif comments.
///
- private void ReadComments()
+ /// The containing image data.
+ private void ReadComments(BufferedReadStream stream)
{
int length;
- var stringBuilder = new StringBuilder();
- while ((length = this.stream.ReadByte()) != 0)
+ StringBuilder stringBuilder = new();
+ while ((length = stream.ReadByte()) != 0)
{
if (length > GifConstants.MaxCommentSubBlockLength)
{
GifThrowHelper.ThrowInvalidImageContentException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentSubBlockLength}' of a comment data block");
}
- if (this.IgnoreMetadata)
+ if (length == -1)
+ {
+ GifThrowHelper.ThrowInvalidImageContentException("Unexpected end of stream while reading gif comment");
+ }
+
+ if (this.skipMetadata)
{
- this.stream.Seek(length, SeekOrigin.Current);
+ stream.Seek(length, SeekOrigin.Current);
continue;
}
- using IMemoryOwner commentsBuffer = this.MemoryAllocator.Allocate(length);
+ using IMemoryOwner commentsBuffer = this.memoryAllocator.Allocate(length);
Span commentsSpan = commentsBuffer.GetSpan();
- this.stream.Read(commentsSpan);
+ stream.Read(commentsSpan);
string commentPart = GifConstants.Encoding.GetString(commentsSpan);
stringBuilder.Append(commentPart);
}
if (stringBuilder.Length > 0)
{
- this.gifMetadata.Comments.Add(stringBuilder.ToString());
+ this.gifMetadata!.Comments.Add(stringBuilder.ToString());
}
}
@@ -370,93 +431,77 @@ private void ReadComments()
/// Reads an individual gif frame.
///
/// The pixel format.
+ /// The containing image data.
/// The image to decode the information to.
/// The previous frame.
- private void ReadFrame(ref Image image, ref ImageFrame previousFrame)
+ private void ReadFrame(BufferedReadStream stream, ref Image? image, ref ImageFrame? previousFrame)
where TPixel : unmanaged, IPixel
{
- this.ReadImageDescriptor();
-
- IMemoryOwner localColorTable = null;
- Buffer2D indices = null;
- try
- {
- // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table.
- if (this.imageDescriptor.LocalColorTableFlag)
- {
- int length = this.imageDescriptor.LocalColorTableSize * 3;
- localColorTable = this.Configuration.MemoryAllocator.Allocate(length, AllocationOptions.Clean);
- this.stream.Read(localColorTable.GetSpan());
- }
+ this.ReadImageDescriptor(stream);
- indices = this.Configuration.MemoryAllocator.Allocate2D(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean);
- this.ReadFrameIndices(indices);
+ // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table.
+ bool hasLocalColorTable = this.imageDescriptor.LocalColorTableFlag;
- Span rawColorTable = default;
- if (localColorTable != null)
- {
- rawColorTable = localColorTable.GetSpan();
- }
- else if (this.globalColorTable != null)
- {
- rawColorTable = this.globalColorTable.GetSpan();
- }
-
- ReadOnlySpan colorTable = MemoryMarshal.Cast(rawColorTable);
- this.ReadFrameColors(ref image, ref previousFrame, indices, colorTable, this.imageDescriptor);
+ if (hasLocalColorTable)
+ {
+ // Read and store the local color table. We allocate the maximum possible size and slice to match.
+ int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
+ this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean);
+ stream.Read(this.currentLocalColorTable.GetSpan().Slice(0, length));
+ }
- // Skip any remaining blocks
- this.SkipBlock();
+ Span rawColorTable = default;
+ if (hasLocalColorTable)
+ {
+ rawColorTable = this.currentLocalColorTable!.GetSpan().Slice(0, this.currentLocalColorTableSize);
}
- finally
+ else if (this.globalColorTable != null)
{
- localColorTable?.Dispose();
- indices?.Dispose();
+ rawColorTable = this.globalColorTable.GetSpan();
}
- }
- ///
- /// Reads the frame indices marking the color to use for each pixel.
- ///
- /// The 2D pixel buffer to write to.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void ReadFrameIndices(Buffer2D indices)
- {
- int minCodeSize = this.stream.ReadByte();
- using var lzwDecoder = new LzwDecoder(this.Configuration.MemoryAllocator, this.stream);
- lzwDecoder.DecodePixels(minCodeSize, indices);
+ ReadOnlySpan colorTable = MemoryMarshal.Cast(rawColorTable);
+ this.ReadFrameColors(stream, ref image, ref previousFrame, colorTable, this.imageDescriptor);
+
+ // Skip any remaining blocks
+ SkipBlock(stream);
}
///
/// Reads the frames colors, mapping indices to colors.
///
/// The pixel format.
+ /// The containing image data.
/// The image to decode the information to.
/// The previous frame.
- /// The indexed pixels.
/// The color table containing the available colors.
/// The
- private void ReadFrameColors(ref Image image, ref ImageFrame previousFrame, Buffer2D indices, ReadOnlySpan colorTable, in GifImageDescriptor descriptor)
+ private void ReadFrameColors(
+ BufferedReadStream stream,
+ ref Image? image,
+ ref ImageFrame? previousFrame,
+ ReadOnlySpan colorTable,
+ in GifImageDescriptor descriptor)
where TPixel : unmanaged, IPixel
{
int imageWidth = this.logicalScreenDescriptor.Width;
int imageHeight = this.logicalScreenDescriptor.Height;
bool transFlag = this.graphicsControlExtension.TransparencyFlag;
- ImageFrame prevFrame = null;
- ImageFrame currentFrame = null;
+ ImageFrame? prevFrame = null;
+ ImageFrame? currentFrame = null;
ImageFrame imageFrame;
if (previousFrame is null)
{
if (!transFlag)
{
- image = new Image(this.Configuration, imageWidth, imageHeight, Color.Black.ToPixel(), this.metadata);
+ image = new Image(this.configuration, imageWidth, imageHeight, Color.Black.ToPixel(), this.metadata);
}
else
{
// This initializes the image to become fully transparent because the alpha channel is zero.
- image = new Image(this.Configuration, imageWidth, imageHeight, this.metadata);
+ image = new Image(this.configuration, imageWidth, imageHeight, this.metadata);
}
this.SetFrameMetadata(image.Frames.RootFrame.Metadata);
@@ -470,7 +515,10 @@ private void ReadFrameColors(ref Image image, ref ImageFrame(ref Image image, ref ImageFrame indicesRowOwner = this.memoryAllocator.Allocate(descriptor.Width);
+ Span indicesRow = indicesRowOwner.Memory.Span;
+ ref byte indicesRowRef = ref MemoryMarshal.GetReference(indicesRow);
+
+ int minCodeSize = stream.ReadByte();
+ if (LzwDecoder.IsValidMinCodeSize(minCodeSize))
{
- ref byte indicesRowRef = ref MemoryMarshal.GetReference(indices.DangerousGetRowSpan(y - descriptorTop));
+ using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream, minCodeSize);
- // Check if this image is interlaced.
- int writeY; // the target y offset to write to
- if (descriptor.InterlaceFlag)
+ for (int y = descriptorTop; y < descriptorBottom && y < imageHeight; y++)
{
- // If so then we read lines at predetermined offsets.
- // When an entire image height worth of offset lines has been read we consider this a pass.
- // With each pass the number of offset lines changes and the starting line changes.
- if (interlaceY >= descriptor.Height)
+ // Check if this image is interlaced.
+ int writeY; // the target y offset to write to
+ if (descriptor.InterlaceFlag)
{
- interlacePass++;
- switch (interlacePass)
+ // If so then we read lines at predetermined offsets.
+ // When an entire image height worth of offset lines has been read we consider this a pass.
+ // With each pass the number of offset lines changes and the starting line changes.
+ if (interlaceY >= descriptor.Height)
{
- case 1:
- interlaceY = 4;
- break;
- case 2:
- interlaceY = 2;
- interlaceIncrement = 4;
- break;
- case 3:
- interlaceY = 1;
- interlaceIncrement = 2;
- break;
+ interlacePass++;
+ switch (interlacePass)
+ {
+ case 1:
+ interlaceY = 4;
+ break;
+ case 2:
+ interlaceY = 2;
+ interlaceIncrement = 4;
+ break;
+ case 3:
+ interlaceY = 1;
+ interlaceIncrement = 2;
+ break;
+ }
}
- }
- writeY = interlaceY + descriptor.Top;
- interlaceY += interlaceIncrement;
- }
- else
- {
- writeY = y;
- }
+ writeY = Math.Min(interlaceY + descriptor.Top, image.Height);
+ interlaceY += interlaceIncrement;
+ }
+ else
+ {
+ writeY = y;
+ }
- ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY));
+ lzwDecoder.DecodePixelRow(indicesRow);
+ ref TPixel rowRef = ref MemoryMarshal.GetReference(imageFrame.PixelBuffer.DangerousGetRowSpan(writeY));
- if (!transFlag)
- {
- // #403 The left + width value can be larger than the image width
- for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
+ if (!transFlag)
{
- int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx);
- ref TPixel pixel = ref Unsafe.Add(ref rowRef, x);
- Rgb24 rgb = colorTable[index];
- pixel.FromRgb24(rgb);
+ // #403 The left + width value can be larger than the image width
+ for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
+ {
+ int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx);
+ ref TPixel pixel = ref Unsafe.Add(ref rowRef, x);
+ Rgb24 rgb = colorTable[index];
+ pixel.FromRgb24(rgb);
+ }
}
- }
- else
- {
- for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
+ else
{
- int index = Numerics.Clamp(Unsafe.Add(ref indicesRowRef, x - descriptorLeft), 0, colorTableMaxIdx);
- if (transIndex != index)
+ for (int x = descriptorLeft; x < descriptorRight && x < imageWidth; x++)
{
+ int index = Unsafe.Add(ref indicesRowRef, x - descriptorLeft);
+
+ // Treat any out of bounds values as transparent.
+ if (index > colorTableMaxIdx || index == transIndex)
+ {
+ continue;
+ }
+
ref TPixel pixel = ref Unsafe.Add(ref rowRef, x);
Rgb24 rgb = colorTable[index];
pixel.FromRgb24(rgb);
@@ -574,6 +637,43 @@ private void ReadFrameColors(ref Image image, ref ImageFrame
+ /// Reads the frames metadata.
+ ///
+ /// The containing image data.
+ /// The collection of frame metadata.
+ /// The previous frame metadata.
+ private void ReadFrameMetadata(BufferedReadStream stream, List frameMetadata, ref ImageFrameMetadata? previousFrame)
+ {
+ this.ReadImageDescriptor(stream);
+
+ // Skip the color table for this frame if local.
+ if (this.imageDescriptor.LocalColorTableFlag)
+ {
+ // Read and store the local color table. We allocate the maximum possible size and slice to match.
+ int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3;
+ this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean);
+ stream.Read(this.currentLocalColorTable.GetSpan().Slice(0, length));
+ }
+
+ // Skip the frame indices. Pixels length + mincode size.
+ // The gif format does not tell us the length of the compressed data beforehand.
+ int minCodeSize = stream.ReadByte();
+ if (LzwDecoder.IsValidMinCodeSize(minCodeSize))
+ {
+ using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream, minCodeSize);
+ lzwDecoder.SkipIndices(this.imageDescriptor.Width * this.imageDescriptor.Height);
+ }
+
+ ImageFrameMetadata currentFrame = new();
+ frameMetadata.Add(currentFrame);
+ this.SetFrameMetadata(currentFrame);
+ previousFrame = currentFrame;
+
+ // Skip any remaining blocks
+ SkipBlock(stream);
+ }
+
///
/// Restores the current frame area to the background.
///
@@ -587,7 +687,7 @@ private void RestoreToBackground(ImageFrame frame)
return;
}
- var interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
+ Rectangle interest = Rectangle.Intersect(frame.Bounds(), this.restoreArea.Value);
Buffer2DRegion pixelRegion = frame.PixelBuffer.GetRegion(interest);
pixelRegion.Clear();
@@ -595,7 +695,7 @@ private void RestoreToBackground(ImageFrame frame)
}
///
- /// Sets the frames metadata.
+ /// Sets the metadata for the image frame.
///
/// The metadata.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -628,13 +728,11 @@ private void SetFrameMetadata(ImageFrameMetadata meta)
/// The stream containing image data.
private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream stream)
{
- this.stream = stream;
-
// Skip the identifier
- this.stream.Skip(6);
- this.ReadLogicalScreenDescriptor();
+ stream.Skip(6);
+ this.ReadLogicalScreenDescriptor(stream);
- var meta = new ImageMetadata();
+ ImageMetadata meta = new();
// The Pixel Aspect Ratio is defined to be the quotient of the pixel's
// width over its height. The value range in this field allows
@@ -671,16 +769,26 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream s
if (this.logicalScreenDescriptor.GlobalColorTableFlag)
{
int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3;
- this.gifMetadata.GlobalColorTableLength = globalColorTableLength;
-
if (globalColorTableLength > 0)
{
- this.globalColorTable = this.MemoryAllocator.Allocate(globalColorTableLength, AllocationOptions.Clean);
+ this.globalColorTable = this.memoryAllocator.Allocate(globalColorTableLength, AllocationOptions.Clean);
- // Read the global color table data from the stream
- stream.Read(this.globalColorTable.GetSpan());
+ // Read the global color table data from the stream and preserve it in the gif metadata
+ Span globalColorTableSpan = this.globalColorTable.GetSpan();
+ stream.Read(globalColorTableSpan);
+
+ //Color[] colorTable = new Color[this.logicalScreenDescriptor.GlobalColorTableSize];
+ //ReadOnlySpan rgbTable = MemoryMarshal.Cast(globalColorTableSpan);
+ //for (int i = 0; i < colorTable.Length; i++)
+ //{
+ // colorTable[i] = new Color(rgbTable[i]);
+ //}
+
+ //this.gifMetadata.GlobalColorTable = colorTable;
}
}
+
+ //this.gifMetadata.BackgroundColorIndex = this.logicalScreenDescriptor.BackgroundColorIndex;
}
}
}
diff --git a/src/ImageSharp/Formats/Gif/GifThrowHelper.cs b/src/ImageSharp/Formats/Gif/GifThrowHelper.cs
index b85bb139ad..ce6d14b44a 100644
--- a/src/ImageSharp/Formats/Gif/GifThrowHelper.cs
+++ b/src/ImageSharp/Formats/Gif/GifThrowHelper.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Formats.Gif
@@ -24,5 +25,11 @@ public static void ThrowInvalidImageContentException(string errorMessage)
/// if no inner exception is specified.
[MethodImpl(InliningOptions.ColdPath)]
public static void ThrowInvalidImageContentException(string errorMessage, Exception innerException) => throw new InvalidImageContentException(errorMessage, innerException);
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowNoHeader() => throw new InvalidImageContentException("Gif image does not contain a Logical Screen Descriptor.");
+
+ [MethodImpl(InliningOptions.ColdPath)]
+ public static void ThrowNoData() => throw new InvalidImageContentException("Unable to read Gif image data");
}
}
diff --git a/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs b/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs
index 56bb6d6519..98145e0554 100644
--- a/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs
+++ b/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs
@@ -19,5 +19,10 @@ internal interface IGifDecoderOptions
/// Gets the decoding mode for multi-frame images.
///
FrameDecodingMode DecodingMode { get; }
+
+ ///
+ /// Gets or sets the maximum number of gif frames.
+ ///
+ uint MaxFrames { get; set; }
}
}
diff --git a/src/ImageSharp/Formats/Gif/LzwDecoder.cs b/src/ImageSharp/Formats/Gif/LzwDecoder.cs
index 2a07200016..6ad88f95cf 100644
--- a/src/ImageSharp/Formats/Gif/LzwDecoder.cs
+++ b/src/ImageSharp/Formats/Gif/LzwDecoder.cs
@@ -20,6 +20,11 @@ internal sealed class LzwDecoder : IDisposable
///
private const int MaxStackSize = 4096;
+ ///
+ /// The maximum bits for a lzw code.
+ ///
+ private const int MaximumLzwBits = 12;
+
///
/// The null code.
///
@@ -33,17 +38,36 @@ internal sealed class LzwDecoder : IDisposable
///
/// The prefix buffer.
///
- private readonly IMemoryOwner prefix;
+ private readonly IMemoryOwner prefixOwner;
///
/// The suffix buffer.
///
- private readonly IMemoryOwner suffix;
+ private readonly IMemoryOwner suffixOwner;
+
+ ///
+ /// The scratch buffer for reading data blocks.
+ ///
+ private readonly IMemoryOwner bufferOwner;
///
/// The pixel stack buffer.
///
- private readonly IMemoryOwner pixelStack;
+ private readonly IMemoryOwner pixelStackOwner;
+ private readonly int minCodeSize;
+ private readonly int clearCode;
+ private readonly int endCode;
+ private int code;
+ private int codeSize;
+ private int codeMask;
+ private int availableCode;
+ private int oldCode = NullCode;
+ private int bits;
+ private int top;
+ private int count;
+ private int bufferIndex;
+ private int data;
+ private int first;
///
/// Initializes a new instance of the class
@@ -51,90 +75,236 @@ internal sealed class LzwDecoder : IDisposable
///
/// The to use for buffer allocations.
/// The stream to read from.
+ /// The minimum code size.
/// is null.
- public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream)
+ public LzwDecoder(MemoryAllocator memoryAllocator, BufferedReadStream stream, int minCodeSize)
{
this.stream = stream ?? throw new ArgumentNullException(nameof(stream));
+ Guard.IsTrue(IsValidMinCodeSize(minCodeSize), nameof(minCodeSize), "Invalid minimum code size.");
+
+ this.prefixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean);
+ this.suffixOwner = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean);
+ this.pixelStackOwner = memoryAllocator.Allocate(MaxStackSize + 1, AllocationOptions.Clean);
+ this.bufferOwner = memoryAllocator.Allocate(byte.MaxValue, AllocationOptions.None);
+ this.minCodeSize = minCodeSize;
- this.prefix = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean);
- this.suffix = memoryAllocator.Allocate(MaxStackSize, AllocationOptions.Clean);
- this.pixelStack = memoryAllocator.Allocate(MaxStackSize + 1, AllocationOptions.Clean);
+ // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize
+ this.clearCode = 1 << minCodeSize;
+ this.codeSize = minCodeSize + 1;
+ this.codeMask = (1 << this.codeSize) - 1;
+ this.endCode = this.clearCode + 1;
+ this.availableCode = this.clearCode + 2;
+
+ // Fill the suffix buffer with the initial values represented by the number of colors.
+ Span suffix = this.suffixOwner.GetSpan().Slice(0, this.clearCode);
+ int i;
+ for (i = 0; i < suffix.Length; i++)
+ {
+ suffix[i] = i;
+ }
+
+ this.code = i;
}
///
- /// Decodes and decompresses all pixel indices from the stream.
+ /// Gets a value indicating whether the minimum code size is valid.
///
- /// Minimum code size of the data.
- /// The pixel array to decode to.
- public void DecodePixels(int minCodeSize, Buffer2D pixels)
+ /// The minimum code size.
+ ///
+ /// if the minimum code size is valid; otherwise, .
+ ///
+ public static bool IsValidMinCodeSize(int minCodeSize)
{
- // Calculate the clear code. The value of the clear code is 2 ^ minCodeSize
- int clearCode = 1 << minCodeSize;
-
// It is possible to specify a larger LZW minimum code size than the palette length in bits
// which may leave a gap in the codes where no colors are assigned.
// http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp#lzw_compression
- if (minCodeSize < 2 || clearCode > MaxStackSize)
+ if (minCodeSize < 2 || minCodeSize > MaximumLzwBits || 1 << minCodeSize > MaxStackSize)
{
// Don't attempt to decode the frame indices.
// Theoretically we could determine a min code size from the length of the provided
// color palette but we won't bother since the image is most likely corrupted.
- GifThrowHelper.ThrowInvalidImageContentException("Gif Image does not contain a valid LZW minimum code.");
+ return false;
}
- // The resulting index table length.
- int width = pixels.Width;
- int height = pixels.Height;
- int length = width * height;
+ return true;
+ }
- int codeSize = minCodeSize + 1;
+ ///
+ /// Decodes and decompresses all pixel indices for a single row from the stream, assigning the pixel values to the buffer.
+ ///
+ /// The pixel indices array to decode to.
+ public void DecodePixelRow(Span indices)
+ {
+ indices.Clear();
+
+ // Get span values from the owners.
+ Span prefix = this.prefixOwner.GetSpan();
+ Span suffix = this.suffixOwner.GetSpan();
+ Span pixelStack = this.pixelStackOwner.GetSpan();
+ Span buffer = this.bufferOwner.GetSpan();
+
+ // Cache frequently accessed instance fields into locals.
+ // This helps avoid repeated field loads inside the tight loop.
+ BufferedReadStream stream = this.stream;
+ int top = this.top;
+ int bits = this.bits;
+ int codeSize = this.codeSize;
+ int codeMask = this.codeMask;
+ int minCodeSize = this.minCodeSize;
+ int availableCode = this.availableCode;
+ int oldCode = this.oldCode;
+ int first = this.first;
+ int data = this.data;
+ int count = this.count;
+ int bufferIndex = this.bufferIndex;
+ int code = this.code;
+ int clearCode = this.clearCode;
+ int endCode = this.endCode;
+
+ int i = 0;
+ while (i < indices.Length)
+ {
+ if (top == 0)
+ {
+ if (bits < codeSize)
+ {
+ // Load bytes until there are enough bits for a code.
+ if (count == 0)
+ {
+ // Read a new data block.
+ count = ReadBlock(stream, buffer);
+ if (count == 0)
+ {
+ break;
+ }
- // Calculate the end code
- int endCode = clearCode + 1;
+ bufferIndex = 0;
+ }
- // Calculate the available code.
- int availableCode = clearCode + 2;
+ data += buffer[bufferIndex] << bits;
+ bits += 8;
+ bufferIndex++;
+ count--;
+ continue;
+ }
- // Jillzhangs Code see: http://giflib.codeplex.com/
- // Adapted from John Cristy's ImageMagick.
- int code;
- int oldCode = NullCode;
- int codeMask = (1 << codeSize) - 1;
- int bits = 0;
+ // Get the next code
+ code = data & codeMask;
+ data >>= codeSize;
+ bits -= codeSize;
- int top = 0;
- int count = 0;
- int bi = 0;
- int xyz = 0;
+ // Interpret the code
+ if (code > availableCode || code == endCode)
+ {
+ break;
+ }
- int data = 0;
- int first = 0;
+ if (code == clearCode)
+ {
+ // Reset the decoder
+ codeSize = minCodeSize + 1;
+ codeMask = (1 << codeSize) - 1;
+ availableCode = clearCode + 2;
+ oldCode = NullCode;
+ continue;
+ }
- ref int prefixRef = ref MemoryMarshal.GetReference(this.prefix.GetSpan());
- ref int suffixRef = ref MemoryMarshal.GetReference(this.suffix.GetSpan());
- ref int pixelStackRef = ref MemoryMarshal.GetReference(this.pixelStack.GetSpan());
+ if (oldCode == NullCode)
+ {
+ pixelStack[top++] = suffix[code];
+ oldCode = code;
+ first = code;
+ continue;
+ }
- for (code = 0; code < clearCode; code++)
- {
- Unsafe.Add(ref suffixRef, code) = (byte)code;
- }
+ int inCode = code;
+ if (code == availableCode)
+ {
+ pixelStack[top++] = first;
+ code = oldCode;
+ }
+
+ while (code > clearCode && top < MaxStackSize)
+ {
+ pixelStack[top++] = suffix[code];
+ code = prefix[code];
+ }
- Span buffer = stackalloc byte[byte.MaxValue];
+ int suffixCode = suffix[code];
+ first = suffixCode;
+ pixelStack[top++] = suffixCode;
- int y = 0;
- int x = 0;
- int rowMax = width;
- ref byte pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(y));
- while (xyz < length)
- {
- // Reset row reference.
- if (xyz == rowMax)
- {
- x = 0;
- pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(++y));
- rowMax = (y * width) + width;
+ // Fix for GIFs that have "deferred clear code" as per:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=55918
+ if (availableCode < MaxStackSize)
+ {
+ prefix[availableCode] = oldCode;
+ suffix[availableCode] = first;
+ availableCode++;
+ if (availableCode == codeMask + 1 && availableCode < MaxStackSize)
+ {
+ codeSize++;
+ codeMask = (1 << codeSize) - 1;
+ }
+ }
+
+ oldCode = inCode;
}
+ // Pop a pixel off the pixel stack.
+ top--;
+
+ // Clear missing pixels.
+ indices[i++] = (byte)pixelStack[top];
+ }
+
+ // Write back the local values to the instance fields.
+ this.top = top;
+ this.bits = bits;
+ this.codeSize = codeSize;
+ this.codeMask = codeMask;
+ this.availableCode = availableCode;
+ this.oldCode = oldCode;
+ this.first = first;
+ this.data = data;
+ this.count = count;
+ this.bufferIndex = bufferIndex;
+ this.code = code;
+ }
+
+ ///
+ /// Decodes and decompresses all pixel indices from the stream allowing skipping of the data.
+ ///
+ /// The resulting index table length.
+ public void SkipIndices(int length)
+ {
+ // Get span values from the owners.
+ Span prefix = this.prefixOwner.GetSpan();
+ Span suffix = this.suffixOwner.GetSpan();
+ Span pixelStack = this.pixelStackOwner.GetSpan();
+ Span buffer = this.bufferOwner.GetSpan();
+
+ // Cache frequently accessed instance fields into locals.
+ // This helps avoid repeated field loads inside the tight loop.
+ BufferedReadStream stream = this.stream;
+ int top = this.top;
+ int bits = this.bits;
+ int codeSize = this.codeSize;
+ int codeMask = this.codeMask;
+ int minCodeSize = this.minCodeSize;
+ int availableCode = this.availableCode;
+ int oldCode = this.oldCode;
+ int first = this.first;
+ int data = this.data;
+ int count = this.count;
+ int bufferIndex = this.bufferIndex;
+ int code = this.code;
+ int clearCode = this.clearCode;
+ int endCode = this.endCode;
+
+ int i = 0;
+ while (i < length)
+ {
if (top == 0)
{
if (bits < codeSize)
@@ -143,19 +313,18 @@ public void DecodePixels(int minCodeSize, Buffer2D pixels)
if (count == 0)
{
// Read a new data block.
- count = this.ReadBlock(buffer);
+ count = ReadBlock(stream, buffer);
if (count == 0)
{
break;
}
- bi = 0;
+ bufferIndex = 0;
}
- data += buffer[bi] << bits;
-
+ data += buffer[bufferIndex] << bits;
bits += 8;
- bi++;
+ bufferIndex++;
count--;
continue;
}
@@ -183,7 +352,7 @@ public void DecodePixels(int minCodeSize, Buffer2D pixels)
if (oldCode == NullCode)
{
- Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code);
+ pixelStack[top++] = suffix[code];
oldCode = code;
first = code;
continue;
@@ -192,27 +361,26 @@ public void DecodePixels(int minCodeSize, Buffer2D pixels)
int inCode = code;
if (code == availableCode)
{
- Unsafe.Add(ref pixelStackRef, top++) = (byte)first;
-
+ pixelStack[top++] = first;
code = oldCode;
}
- while (code > clearCode)
+ while (code > clearCode && top < MaxStackSize)
{
- Unsafe.Add(ref pixelStackRef, top++) = Unsafe.Add(ref suffixRef, code);
- code = Unsafe.Add(ref prefixRef, code);
+ pixelStack[top++] = suffix[code];
+ code = prefix[code];
}
- int suffixCode = Unsafe.Add(ref suffixRef, code);
+ int suffixCode = suffix[code];
first = suffixCode;
- Unsafe.Add(ref pixelStackRef, top++) = suffixCode;
+ pixelStack[top++] = suffixCode;
- // Fix for Gifs that have "deferred clear code" as per here :
+ // Fix for GIFs that have "deferred clear code" as per:
// https://bugzilla.mozilla.org/show_bug.cgi?id=55918
if (availableCode < MaxStackSize)
{
- Unsafe.Add(ref prefixRef, availableCode) = oldCode;
- Unsafe.Add(ref suffixRef, availableCode) = first;
+ prefix[availableCode] = oldCode;
+ suffix[availableCode] = first;
availableCode++;
if (availableCode == codeMask + 1 && availableCode < MaxStackSize)
{
@@ -227,31 +395,44 @@ public void DecodePixels(int minCodeSize, Buffer2D pixels)
// Pop a pixel off the pixel stack.
top--;
- // Clear missing pixels
- xyz++;
- Unsafe.Add(ref pixelsRowRef, x++) = (byte)Unsafe.Add(ref pixelStackRef, top);
+ // Skip missing pixels.
+ i++;
}
+
+ // Write back the local values to the instance fields.
+ this.top = top;
+ this.bits = bits;
+ this.codeSize = codeSize;
+ this.codeMask = codeMask;
+ this.availableCode = availableCode;
+ this.oldCode = oldCode;
+ this.first = first;
+ this.data = data;
+ this.count = count;
+ this.bufferIndex = bufferIndex;
+ this.code = code;
}
///
/// Reads the next data block from the stream. A data block begins with a byte,
/// which defines the size of the block, followed by the block itself.
///
+ /// The stream to read from.
/// The buffer to store the block in.
///
/// The .
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private int ReadBlock(Span buffer)
+ private static int ReadBlock(BufferedReadStream stream, Span buffer)
{
- int bufferSize = this.stream.ReadByte();
+ int bufferSize = stream.ReadByte();
if (bufferSize < 1)
{
return 0;
}
- int count = this.stream.Read(buffer, 0, bufferSize);
+ int count = stream.Read(buffer, 0, bufferSize);
return count != bufferSize ? 0 : bufferSize;
}
@@ -259,9 +440,10 @@ private int ReadBlock(Span buffer)
///
public void Dispose()
{
- this.prefix.Dispose();
- this.suffix.Dispose();
- this.pixelStack.Dispose();
+ this.prefixOwner.Dispose();
+ this.suffixOwner.Dispose();
+ this.pixelStackOwner.Dispose();
+ this.bufferOwner.Dispose();
}
}
}
diff --git a/src/ImageSharp/Formats/ImageDecoderUtilities.cs b/src/ImageSharp/Formats/ImageDecoderUtilities.cs
index 71ecda8938..a2bbe34058 100644
--- a/src/ImageSharp/Formats/ImageDecoderUtilities.cs
+++ b/src/ImageSharp/Formats/ImageDecoderUtilities.cs
@@ -46,7 +46,8 @@ public static Image Decode(
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
- using var bufferedReadStream = new BufferedReadStream(configuration, stream);
+ // Test may pass a BufferedReadStream in order to monitor EOF hits, if so, use the existing instance.
+ BufferedReadStream bufferedReadStream = stream as BufferedReadStream ?? new BufferedReadStream(configuration, stream);
try
{
@@ -56,6 +57,13 @@ public static Image Decode(
{
throw largeImageExceptionFactory(ex, decoder.Dimensions);
}
+ finally
+ {
+ if (bufferedReadStream != stream)
+ {
+ bufferedReadStream.Dispose();
+ }
+ }
}
private static InvalidImageContentException DefaultLargeImageExceptionFactory(
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
index 3664cb4eb3..c6fe34e22e 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanBuffer.cs
@@ -22,6 +22,9 @@ internal struct HuffmanScanBuffer
// Whether there is no more good data to pull from the stream for the current mcu.
private bool badData;
+ // How many times have we hit the eof.
+ private int eofHitCount;
+
public HuffmanScanBuffer(BufferedReadStream stream)
{
this.stream = stream;
@@ -31,6 +34,7 @@ public HuffmanScanBuffer(BufferedReadStream stream)
this.MarkerPosition = 0;
this.badData = false;
this.NoData = false;
+ this.eofHitCount = 0;
}
///
@@ -211,13 +215,23 @@ public bool FindNextMarker()
private int ReadStream()
{
int value = this.badData ? 0 : this.stream.ReadByte();
- if (value == -1)
+
+ // We've encountered the end of the file stream which means there's no EOI marker or the marker has been read
+ // during decoding of the SOS marker.
+ // When reading individual bits 'badData' simply means we have hit a marker, When data is '0' and the stream is exhausted
+ // we know we have hit the EOI and completed decoding the scan buffer.
+ if (value == -1 || (this.badData && this.data == 0 && this.stream.Position >= this.stream.Length))
{
- // We've encountered the end of the file stream which means there's no EOI marker
+ // We've hit the end of the file stream more times than allowed which means there's no EOI marker
// in the image or the SOS marker has the wrong dimensions set.
- this.badData = true;
- this.NoData = true;
- value = 0;
+ if (this.eofHitCount > JpegConstants.Huffman.FetchLoop)
+ {
+ this.badData = true;
+ this.NoData = true;
+ value = 0;
+ }
+
+ this.eofHitCount++;
}
return value;
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
index 532892e060..220bc16798 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
@@ -116,7 +116,8 @@ public override void InjectFrameData(JpegFrame frame, IRawJpegData jpegData)
this.pixelBuffer = allocator.Allocate2D(
frame.PixelWidth,
frame.PixelHeight,
- this.configuration.PreferContiguousImageBuffers);
+ this.configuration.PreferContiguousImageBuffers,
+ AllocationOptions.Clean);
this.paddedProxyPixelRow = allocator.Allocate(frame.PixelWidth + 3);
// component processors from spectral to Rgba32
@@ -215,4 +216,4 @@ public void Dispose()
this.pixelBuffer?.Dispose();
}
}
-}
+}
\ No newline at end of file
diff --git a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs
index eab5e6a082..fc35f82b7a 100644
--- a/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs
+++ b/src/ImageSharp/Formats/Jpeg/Components/Quantization.cs
@@ -147,7 +147,7 @@ public static int EstimateQuality(ref Block8x8F table, ReadOnlySpan target
quality = (int)Math.Round(5000.0 / sumPercent);
}
- return quality;
+ return Numerics.Clamp(quality, MinQualityFactor, MaxQualityFactor);
}
///
diff --git a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs
index 33af30434c..25563d2d95 100644
--- a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs
+++ b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs
@@ -72,7 +72,11 @@ private static void ProcessGrayscale(Configuration configuration, Buffer
for (int y = 0; y < height; y++)
{
- stream.Read(rowSpan);
+ if (stream.Read(rowSpan) < rowSpan.Length)
+ {
+ return;
+ }
+
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.FromL8Bytes(
configuration,
@@ -94,7 +98,11 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu
for (int y = 0; y < height; y++)
{
- stream.Read(rowSpan);
+ if (stream.Read(rowSpan) < rowSpan.Length)
+ {
+ return;
+ }
+
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.FromL16Bytes(
configuration,
@@ -116,7 +124,11 @@ private static void ProcessRgb(Configuration configuration, Buffer2D pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.FromRgb24Bytes(
configuration,
@@ -138,7 +150,11 @@ private static void ProcessWideRgb(Configuration configuration, Buffer2D
for (int y = 0; y < height; y++)
{
- stream.Read(rowSpan);
+ if (stream.Read(rowSpan) < rowSpan.Length)
+ {
+ return;
+ }
+
Span pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations.Instance.FromRgb48Bytes(
configuration,
@@ -153,7 +169,6 @@ private static void ProcessBlackAndWhite(Configuration configuration, Bu
{
int width = pixels.Width;
int height = pixels.Height;
- int startBit = 0;
MemoryAllocator allocator = configuration.MemoryAllocator;
using IMemoryOwner row = allocator.Allocate(width);
Span rowSpan = row.GetSpan();
@@ -163,23 +178,17 @@ private static void ProcessBlackAndWhite(Configuration configuration, Bu
for (int x = 0; x < width;)
{
int raw = stream.ReadByte();
- int bit = startBit;
- startBit = 0;
- for (; bit < 8; bit++)
+ if (raw < 0)
+ {
+ return;
+ }
+
+ int stopBit = Math.Min(8, width - x);
+ for (int bit = 0; bit < stopBit; bit++)
{
bool bitValue = (raw & (0x80 >> bit)) != 0;
rowSpan[x] = bitValue ? black : white;
x++;
- if (x == width)
- {
- startBit = (bit + 1) & 7; // Round off to below 8.
- if (startBit != 0)
- {
- stream.Seek(-1, System.IO.SeekOrigin.Current);
- }
-
- break;
- }
}
}
diff --git a/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs
index 581d3e592b..94468f90aa 100644
--- a/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs
+++ b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs
@@ -12,14 +12,20 @@ namespace SixLabors.ImageSharp.Formats.Pbm
internal static class BufferedReadStreamExtensions
{
///
- /// Skip over any whitespace or any comments.
+ /// Skip over any whitespace or any comments and signal if EOF has been reached.
///
- public static void SkipWhitespaceAndComments(this BufferedReadStream stream)
+ /// The buffered read stream.
+ /// if EOF has been reached while reading the stream; see langword="true"/> otherwise.
+ public static bool SkipWhitespaceAndComments(this BufferedReadStream stream)
{
bool isWhitespace;
do
{
int val = stream.ReadByte();
+ if (val < 0)
+ {
+ return false;
+ }
// Comments start with '#' and end at the next new-line.
if (val == 0x23)
@@ -28,8 +34,12 @@ public static void SkipWhitespaceAndComments(this BufferedReadStream stream)
do
{
innerValue = stream.ReadByte();
+ if (innerValue < 0)
+ {
+ return false;
+ }
}
- while (innerValue != 0x0a);
+ while (innerValue is not 0x0a);
// Continue searching for whitespace.
val = innerValue;
@@ -39,18 +49,31 @@ public static void SkipWhitespaceAndComments(this BufferedReadStream stream)
}
while (isWhitespace);
stream.Seek(-1, SeekOrigin.Current);
+ return true;
}
///
- /// Read a decimal text value.
+ /// Read a decimal text value and signal if EOF has been reached.
///
- /// The integer value of the decimal.
- public static int ReadDecimal(this BufferedReadStream stream)
+ /// The buffered read stream.
+ /// The read value.
+ /// if EOF has been reached while reading the stream; otherwise.
+ ///
+ /// A 'false' return value doesn't mean that the parsing has been failed, since it's possible to reach EOF while reading the last decimal in the file.
+ /// It's up to the call site to handle such a situation.
+ ///
+ public static bool ReadDecimal(this BufferedReadStream stream, out int value)
{
- int value = 0;
+ value = 0;
while (true)
{
- int current = stream.ReadByte() - 0x30;
+ int current = stream.ReadByte();
+ if (current < 0)
+ {
+ return false;
+ }
+
+ current -= 0x30;
if ((uint)current > 9)
{
break;
@@ -59,7 +82,7 @@ public static int ReadDecimal(this BufferedReadStream stream)
value = (value * 10) + current;
}
- return value;
+ return true;
}
}
}
diff --git a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
index 749fc0292b..ccd5041239 100644
--- a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
+++ b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
@@ -90,6 +90,7 @@ public IImageInfo Identify(BufferedReadStream stream, CancellationToken cancella
/// Processes the ppm header.
///
/// The input stream.
+ /// An EOF marker has been read before the image has been decoded.
private void ProcessHeader(BufferedReadStream stream)
{
Span buffer = stackalloc byte[2];
@@ -139,14 +140,22 @@ private void ProcessHeader(BufferedReadStream stream)
throw new InvalidImageContentException("Unknown of not implemented image type encountered.");
}
- stream.SkipWhitespaceAndComments();
- int width = stream.ReadDecimal();
- stream.SkipWhitespaceAndComments();
- int height = stream.ReadDecimal();
- stream.SkipWhitespaceAndComments();
+ if (!stream.SkipWhitespaceAndComments() ||
+ !stream.ReadDecimal(out int width) ||
+ !stream.SkipWhitespaceAndComments() ||
+ !stream.ReadDecimal(out int height) ||
+ !stream.SkipWhitespaceAndComments())
+ {
+ ThrowPrematureEof();
+ }
+
if (this.ColorType != PbmColorType.BlackAndWhite)
{
- this.maxPixelValue = stream.ReadDecimal();
+ if (!stream.ReadDecimal(out this.maxPixelValue))
+ {
+ ThrowPrematureEof();
+ }
+
if (this.maxPixelValue > 255)
{
this.ComponentType = PbmComponentType.Short;
@@ -169,6 +178,8 @@ private void ProcessHeader(BufferedReadStream stream)
meta.Encoding = this.Encoding;
meta.ColorType = this.ColorType;
meta.ComponentType = this.ComponentType;
+
+ static void ThrowPrematureEof() => throw new InvalidImageContentException("Reached EOF while reading the header.");
}
private void ProcessPixels(BufferedReadStream stream, Buffer2D pixels)
diff --git a/src/ImageSharp/Formats/Pbm/PlainDecoder.cs b/src/ImageSharp/Formats/Pbm/PlainDecoder.cs
index aeb527dd20..f5e0378cea 100644
--- a/src/ImageSharp/Formats/Pbm/PlainDecoder.cs
+++ b/src/ImageSharp/Formats/Pbm/PlainDecoder.cs
@@ -66,13 +66,18 @@ private static void ProcessGrayscale(Configuration configuration, Buffer
using IMemoryOwner row = allocator.Allocate(width);
Span rowSpan = row.GetSpan();
+ bool eofReached = false;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
- byte value = (byte)stream.ReadDecimal();
- stream.SkipWhitespaceAndComments();
- rowSpan[x] = new L8(value);
+ stream.ReadDecimal(out int value);
+ rowSpan[x] = new L8((byte)value);
+ eofReached = !stream.SkipWhitespaceAndComments();
+ if (eofReached)
+ {
+ break;
+ }
}
Span pixelSpan = pixels.DangerousGetRowSpan(y);
@@ -80,6 +85,11 @@ private static void ProcessGrayscale(Configuration configuration, Buffer
configuration,
rowSpan,
pixelSpan);
+
+ if (eofReached)
+ {
+ return;
+ }
}
}
@@ -92,13 +102,18 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu
using IMemoryOwner row = allocator.Allocate(width);
Span rowSpan = row.GetSpan();
+ bool eofReached = false;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
- ushort value = (ushort)stream.ReadDecimal();
- stream.SkipWhitespaceAndComments();
- rowSpan[x] = new L16(value);
+ stream.ReadDecimal(out int value);
+ rowSpan[x] = new L16((ushort)value);
+ eofReached = !stream.SkipWhitespaceAndComments();
+ if (eofReached)
+ {
+ break;
+ }
}
Span pixelSpan = pixels.DangerousGetRowSpan(y);
@@ -106,6 +121,11 @@ private static void ProcessWideGrayscale(Configuration configuration, Bu
configuration,
rowSpan,
pixelSpan);
+
+ if (eofReached)
+ {
+ return;
+ }
}
}
@@ -118,17 +138,29 @@ private static void ProcessRgb(Configuration configuration, Buffer2D row = allocator.Allocate(width);
Span rowSpan = row.GetSpan();
+ bool eofReached = false;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
- byte red = (byte)stream.ReadDecimal();
- stream.SkipWhitespaceAndComments();
- byte green = (byte)stream.ReadDecimal();
- stream.SkipWhitespaceAndComments();
- byte blue = (byte)stream.ReadDecimal();
- stream.SkipWhitespaceAndComments();
- rowSpan[x] = new Rgb24(red, green, blue);
+ if (!stream.ReadDecimal(out int red) ||
+ !stream.SkipWhitespaceAndComments() ||
+ !stream.ReadDecimal(out int green) ||
+ !stream.SkipWhitespaceAndComments())
+ {
+ // Reached EOF before reading a full RGB value
+ eofReached = true;
+ break;
+ }
+
+ stream.ReadDecimal(out int blue);
+
+ rowSpan[x] = new Rgb24((byte)red, (byte)green, (byte)blue);
+ eofReached = !stream.SkipWhitespaceAndComments();
+ if (eofReached)
+ {
+ break;
+ }
}
Span pixelSpan = pixels.DangerousGetRowSpan(y);
@@ -136,6 +168,11 @@ private static void ProcessRgb(Configuration configuration, Buffer2D(Configuration configuration, Buffer2D
using IMemoryOwner row = allocator.Allocate(width);
Span rowSpan = row.GetSpan();
+ bool eofReached = false;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
- ushort red = (ushort)stream.ReadDecimal();
- stream.SkipWhitespaceAndComments();
- ushort green = (ushort)stream.ReadDecimal();
- stream.SkipWhitespaceAndComments();
- ushort blue = (ushort)stream.ReadDecimal();
- stream.SkipWhitespaceAndComments();
- rowSpan[x] = new Rgb48(red, green, blue);
+ if (!stream.ReadDecimal(out int red) ||
+ !stream.SkipWhitespaceAndComments() ||
+ !stream.ReadDecimal(out int green) ||
+ !stream.SkipWhitespaceAndComments())
+ {
+ // Reached EOF before reading a full RGB value
+ eofReached = true;
+ break;
+ }
+
+ stream.ReadDecimal(out int blue);
+
+ rowSpan[x] = new Rgb48((ushort)red, (ushort)green, (ushort)blue);
+ eofReached = !stream.SkipWhitespaceAndComments();
+ if (eofReached)
+ {
+ break;
+ }
}
Span pixelSpan = pixels.DangerousGetRowSpan(y);
@@ -166,6 +215,11 @@ private static void ProcessWideRgb(Configuration configuration, Buffer2D
configuration,
rowSpan,
pixelSpan);
+
+ if (eofReached)
+ {
+ return;
+ }
}
}
@@ -178,13 +232,19 @@ private static void ProcessBlackAndWhite(Configuration configuration, Bu
using IMemoryOwner row = allocator.Allocate(width);
Span rowSpan = row.GetSpan();
+ bool eofReached = false;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
- int value = stream.ReadDecimal();
- stream.SkipWhitespaceAndComments();
+ stream.ReadDecimal(out int value);
+
rowSpan[x] = value == 0 ? White : Black;
+ eofReached = !stream.SkipWhitespaceAndComments();
+ if (eofReached)
+ {
+ break;
+ }
}
Span pixelSpan = pixels.DangerousGetRowSpan(y);
@@ -192,6 +252,11 @@ private static void ProcessBlackAndWhite(Configuration configuration, Bu
configuration,
rowSpan,
pixelSpan);
+
+ if (eofReached)
+ {
+ return;
+ }
}
}
}
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 12770bc521..d6c3256e06 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -464,7 +464,7 @@ private void ReadGammaChunk(PngMetadata pngMetadata, ReadOnlySpan data)
private void InitializeImage(ImageMetadata metadata, out Image image)
where TPixel : unmanaged, IPixel
{
- image = Image.CreateUninitialized(
+ image = new Image(
this.Configuration,
this.header.Width,
this.header.Height,
@@ -1504,6 +1504,9 @@ private void SkipChunkDataAndCrc(in PngChunk chunk)
private IMemoryOwner ReadChunkData(int length)
{
// We rent the buffer here to return it afterwards in Decode()
+ // We don't want to throw a degenerated memory exception here as we want to allow partial decoding
+ // so limit the length.
+ length = (int)Math.Min(length, this.currentStream.Length - this.currentStream.Position);
IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(length, AllocationOptions.Clean);
this.currentStream.Read(buffer.GetSpan(), 0, length);
diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
index 26bc566d65..afb89836c0 100644
--- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
+++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs
@@ -250,6 +250,7 @@ public static void ProcessPaletteScanline(
ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan);
ReadOnlySpan palettePixels = MemoryMarshal.Cast(palette);
ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels);
+ int maxIndex = palettePixels.Length - 1;
if (paletteAlpha?.Length > 0)
{
@@ -260,7 +261,7 @@ public static void ProcessPaletteScanline(
for (int x = 0; x < header.Width; x++)
{
- int index = Unsafe.Add(ref scanlineSpanRef, x);
+ int index = Numerics.Clamp(Unsafe.Add(ref scanlineSpanRef, x), 0, maxIndex);
rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index);
rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue;
@@ -272,8 +273,8 @@ public static void ProcessPaletteScanline(
{
for (int x = 0; x < header.Width; x++)
{
- int index = Unsafe.Add(ref scanlineSpanRef, x);
- Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index);
+ uint index = Unsafe.Add(ref scanlineSpanRef, x);
+ Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, (int)Math.Min(index, maxIndex));
pixel.FromRgb24(rgb);
Unsafe.Add(ref rowSpanRef, x) = pixel;
diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
index d101ccd94a..523ead871e 100644
--- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
+++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
@@ -97,7 +97,7 @@ public Image Decode(BufferedReadStream stream, CancellationToken
throw new UnknownImageFormatException("Width or height cannot be 0");
}
- var image = Image.CreateUninitialized(this.Configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata);
+ Image image = new(this.Configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata);
Buffer2D pixels = image.GetRootFramePixelBuffer();
if (this.fileHeader.ColorMapType == 1)
diff --git a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
index cce1d278cc..1d7592679c 100644
--- a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
+++ b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs
@@ -43,7 +43,7 @@ public DirectoryReader(Stream stream, MemoryAllocator allocator)
public IEnumerable Read()
{
this.ByteOrder = ReadByteOrder(this.stream);
- var headerReader = new HeaderReader(this.stream, this.ByteOrder);
+ HeaderReader headerReader = new(this.stream, this.ByteOrder);
headerReader.ReadFileHeader();
this.nextIfdOffset = headerReader.FirstIfdOffset;
@@ -55,7 +55,12 @@ public IEnumerable Read()
private static ByteOrder ReadByteOrder(Stream stream)
{
Span headerBytes = stackalloc byte[2];
- stream.Read(headerBytes);
+
+ if (stream.Read(headerBytes) != 2)
+ {
+ throw TiffThrowHelper.ThrowInvalidHeader();
+ }
+
if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian)
{
return ByteOrder.LittleEndian;
@@ -74,7 +79,7 @@ private IEnumerable ReadIfds(bool isBigTiff)
var readers = new List();
while (this.nextIfdOffset != 0 && this.nextIfdOffset < (ulong)this.stream.Length)
{
- var reader = new EntryReader(this.stream, this.ByteOrder, this.allocator);
+ EntryReader reader = new(this.stream, this.ByteOrder, this.allocator);
reader.ReadTags(isBigTiff, this.nextIfdOffset);
if (reader.BigValues.Count > 0)
@@ -88,6 +93,11 @@ private IEnumerable ReadIfds(bool isBigTiff)
}
}
+ if (this.nextIfdOffset >= reader.NextIfdOffset && reader.NextIfdOffset != 0)
+ {
+ TiffThrowHelper.ThrowImageFormatException("TIFF image contains circular directory offsets");
+ }
+
this.nextIfdOffset = reader.NextIfdOffset;
readers.Add(reader);
diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
index 695359e5ea..37cb9a4fc4 100644
--- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
+++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
@@ -329,60 +329,68 @@ public void Encode(Image image, Stream stream)
bool alphaCompressionSucceeded = false;
using var alphaEncoder = new AlphaEncoder();
Span alphaData = Span.Empty;
- if (hasAlpha)
+ IMemoryOwner encodedAlphaData = null;
+ try
{
- // TODO: This can potentially run in an separate task.
- IMemoryOwner encodedAlphaData = alphaEncoder.EncodeAlpha(image, this.configuration, this.memoryAllocator, this.alphaCompression, out alphaDataSize);
- alphaData = encodedAlphaData.GetSpan();
- if (alphaDataSize < pixelCount)
+ if (hasAlpha)
{
- // Only use compressed data, if the compressed data is actually smaller then the uncompressed data.
- alphaCompressionSucceeded = true;
+ // TODO: This can potentially run in an separate task.
+ encodedAlphaData = alphaEncoder.EncodeAlpha(image, this.configuration, this.memoryAllocator, this.alphaCompression, out alphaDataSize);
+ alphaData = encodedAlphaData.GetSpan();
+ if (alphaDataSize < pixelCount)
+ {
+ // Only use compressed data, if the compressed data is actually smaller then the uncompressed data.
+ alphaCompressionSucceeded = true;
+ }
}
- }
- // Stats-collection loop.
- this.StatLoop(width, height, yStride, uvStride);
- it.Init();
- it.InitFilter();
- var info = new Vp8ModeScore();
- var residual = new Vp8Residual();
- do
- {
- bool dontUseSkip = !this.Proba.UseSkipProba;
- info.Clear();
- it.Import(y, u, v, yStride, uvStride, width, height, false);
-
- // Warning! order is important: first call VP8Decimate() and
- // *then* decide how to code the skip decision if there's one.
- if (!this.Decimate(it, ref info, this.rdOptLevel) || dontUseSkip)
- {
- this.CodeResiduals(it, info, residual);
- }
- else
+ // Stats-collection loop.
+ this.StatLoop(width, height, yStride, uvStride);
+ it.Init();
+ it.InitFilter();
+ var info = new Vp8ModeScore();
+ var residual = new Vp8Residual();
+ do
{
- it.ResetAfterSkip();
+ bool dontUseSkip = !this.Proba.UseSkipProba;
+ info.Clear();
+ it.Import(y, u, v, yStride, uvStride, width, height, false);
+
+ // Warning! order is important: first call VP8Decimate() and
+ // *then* decide how to code the skip decision if there's one.
+ if (!this.Decimate(it, ref info, this.rdOptLevel) || dontUseSkip)
+ {
+ this.CodeResiduals(it, info, residual);
+ }
+ else
+ {
+ it.ResetAfterSkip();
+ }
+
+ it.SaveBoundary();
}
+ while (it.Next());
- it.SaveBoundary();
+ // Store filter stats.
+ this.AdjustFilterStrength();
+
+ // Write bytes from the bitwriter buffer to the stream.
+ ImageMetadata metadata = image.Metadata;
+ metadata.SyncProfiles();
+ this.bitWriter.WriteEncodedImageToStream(
+ stream,
+ metadata.ExifProfile,
+ metadata.XmpProfile,
+ (uint)width,
+ (uint)height,
+ hasAlpha,
+ alphaData.Slice(0, alphaDataSize),
+ this.alphaCompression && alphaCompressionSucceeded);
+ }
+ finally
+ {
+ encodedAlphaData?.Dispose();
}
- while (it.Next());
-
- // Store filter stats.
- this.AdjustFilterStrength();
-
- // Write bytes from the bitwriter buffer to the stream.
- ImageMetadata metadata = image.Metadata;
- metadata.SyncProfiles();
- this.bitWriter.WriteEncodedImageToStream(
- stream,
- metadata.ExifProfile,
- metadata.XmpProfile,
- (uint)width,
- (uint)height,
- hasAlpha,
- alphaData,
- this.alphaCompression && alphaCompressionSucceeded);
}
///
diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs
index 2823b8ed6f..28103359e6 100644
--- a/src/ImageSharp/IO/BufferedReadStream.cs
+++ b/src/ImageSharp/IO/BufferedReadStream.cs
@@ -65,6 +65,11 @@ public BufferedReadStream(Configuration configuration, Stream stream)
this.readBufferIndex = this.BufferSize;
}
+ ///
+ /// Gets the number indicating the EOF hits occured while reading from this instance.
+ ///
+ public int EofHitCount { get; private set; }
+
///
/// Gets the size, in bytes, of the underlying buffer.
///
@@ -138,6 +143,7 @@ public override int ReadByte()
{
if (this.readerPosition >= this.Length)
{
+ this.EofHitCount++;
return -1;
}
@@ -303,7 +309,7 @@ private int ReadToBufferViaCopyFast(Span buffer)
this.readerPosition += n;
this.readBufferIndex += n;
-
+ this.CheckEof(n);
return n;
}
@@ -361,6 +367,7 @@ private int ReadToBufferDirectSlow(Span buffer)
this.Position += n;
+ this.CheckEof(n);
return n;
}
@@ -427,5 +434,14 @@ private unsafe void CopyBytes(byte[] buffer, int offset, int count)
Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count);
}
}
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void CheckEof(int read)
+ {
+ if (read == 0)
+ {
+ this.EofHitCount++;
+ }
+ }
}
}
diff --git a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
index 59d4d5bda4..df8dfbe0a0 100644
--- a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
+++ b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs
@@ -14,7 +14,7 @@ namespace SixLabors.ImageSharp.Memory.Internals
internal struct UnmanagedMemoryHandle : IEquatable
{
// Number of allocation re-attempts when detecting OutOfMemoryException.
- private const int MaxAllocationAttempts = 1000;
+ private const int MaxAllocationAttempts = 10;
// Track allocations for testing purposes:
private static int totalOutstandingHandles;
diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
index 4df78d9d93..c67af18c3d 100644
--- a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
+++ b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
@@ -3,6 +3,8 @@
using System;
using System.Buffers;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
namespace SixLabors.ImageSharp.Memory
{
@@ -11,6 +13,8 @@ namespace SixLabors.ImageSharp.Memory
///
public abstract class MemoryAllocator
{
+ private const int OneGigabyte = 1 << 30;
+
///
/// Gets the default platform-specific global instance that
/// serves as the default value for .
@@ -21,6 +25,10 @@ public abstract class MemoryAllocator
///
public static MemoryAllocator Default { get; } = Create();
+ internal long MemoryGroupAllocationLimitBytes { get; private set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte;
+
+ internal int SingleBufferAllocationLimitBytes { get; private set; } = OneGigabyte;
+
///
/// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes.
///
@@ -31,16 +39,24 @@ public abstract class MemoryAllocator
/// Creates a default instance of a optimized for the executing platform.
///
/// The .
- public static MemoryAllocator Create() =>
- new UniformUnmanagedMemoryPoolMemoryAllocator(null);
+ public static MemoryAllocator Create() => Create(default);
///
/// Creates the default using the provided options.
///
/// The .
/// The .
- public static MemoryAllocator Create(MemoryAllocatorOptions options) =>
- new UniformUnmanagedMemoryPoolMemoryAllocator(options.MaximumPoolSizeMegabytes);
+ public static MemoryAllocator Create(MemoryAllocatorOptions options)
+ {
+ UniformUnmanagedMemoryPoolMemoryAllocator allocator = new(options.MaximumPoolSizeMegabytes);
+ if (options.AllocationLimitMegabytes.HasValue)
+ {
+ allocator.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L;
+ allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes);
+ }
+
+ return allocator;
+ }
///
/// Allocates an , holding a of length .
@@ -65,16 +81,35 @@ public virtual void ReleaseRetainedResources()
///
/// Allocates a .
///
+ /// The type of element to allocate.
/// The total length of the buffer.
/// The expected alignment (eg. to make sure image rows fit into single buffers).
/// The .
/// A new .
/// Thrown when 'blockAlignment' converted to bytes is greater than the buffer capacity of the allocator.
- internal virtual MemoryGroup AllocateGroup(
+ internal MemoryGroup AllocateGroup(
long totalLength,
int bufferAlignment,
AllocationOptions options = AllocationOptions.None)
where T : struct
- => MemoryGroup.Allocate(this, totalLength, bufferAlignment, options);
+ {
+ if (totalLength < 0)
+ {
+ throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={totalLength}.");
+ }
+
+ ulong totalLengthInBytes = (ulong)totalLength * (ulong)Unsafe.SizeOf();
+ if (totalLengthInBytes > (ulong)this.MemoryGroupAllocationLimitBytes)
+ {
+ throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={totalLengthInBytes} that exceeded the limit {this.MemoryGroupAllocationLimitBytes}.");
+ }
+
+ // Cast to long is safe because we already checked that the total length is within the limit.
+ return this.AllocateGroupCore(totalLength, (long)totalLengthInBytes, bufferAlignment, options);
+ }
+
+ internal virtual MemoryGroup AllocateGroupCore(long totalLengthInElements, long totalLengthInBytes, int bufferAlignment, AllocationOptions options)
+ where T : struct
+ => MemoryGroup.Allocate(this, totalLengthInElements, bufferAlignment, options);
}
}
diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
index 22a0410755..70dc515fe4 100644
--- a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
+++ b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Memory
@@ -9,6 +9,7 @@ namespace SixLabors.ImageSharp.Memory
public struct MemoryAllocatorOptions
{
private int? maximumPoolSizeMegabytes;
+ private int? allocationLimitMegabytes;
///
/// Gets or sets a value defining the maximum size of the 's internal memory pool
@@ -27,5 +28,23 @@ public int? MaximumPoolSizeMegabytes
this.maximumPoolSizeMegabytes = value;
}
}
+
+ ///
+ /// Gets or sets a value defining the maximum (discontiguous) buffer size that can be allocated by the allocator in Megabytes.
+ /// means platform default: 1GB on 32-bit processes, 4GB on 64-bit processes.
+ ///
+ public int? AllocationLimitMegabytes
+ {
+ readonly get => this.allocationLimitMegabytes;
+ set
+ {
+ if (value.HasValue)
+ {
+ Guard.MustBeGreaterThan(value.Value, 0, nameof(this.AllocationLimitMegabytes));
+ }
+
+ this.allocationLimitMegabytes = value;
+ }
+ }
}
}
diff --git a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
index a53ecbc66e..e2b132139b 100644
--- a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
+++ b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
@@ -1,7 +1,8 @@
-// Copyright (c) Six Labors.
+// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Buffers;
+using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Memory
@@ -17,7 +18,16 @@ public sealed class SimpleGcMemoryAllocator : MemoryAllocator
///
public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None)
{
- Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
+ if (length < 0)
+ {
+ throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={length}.");
+ }
+
+ ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf();
+ if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
+ {
+ throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={lengthInBytes} that exceeded the limit {this.SingleBufferAllocationLimitBytes}.");
+ }
return new BasicArrayBuffer(new T[length]);
}
diff --git a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
index 0da4ff9f8c..a8056db537 100644
--- a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
+++ b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
@@ -87,10 +87,18 @@ public override IMemoryOwner Allocate(
int length,
AllocationOptions options = AllocationOptions.None)
{
- Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
- int lengthInBytes = length * Unsafe.SizeOf();
+ if (length < 0)
+ {
+ throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={length}.");
+ }
+
+ ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf();
+ if (lengthInBytes > (ulong)this.SingleBufferAllocationLimitBytes)
+ {
+ throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of length={lengthInBytes} that exceeded the limit {this.SingleBufferAllocationLimitBytes}.");
+ }
- if (lengthInBytes <= this.sharedArrayPoolThresholdInBytes)
+ if (lengthInBytes <= (ulong)this.sharedArrayPoolThresholdInBytes)
{
var buffer = new SharedArrayPoolBuffer(length);
if (options.Has(AllocationOptions.Clean))
@@ -101,7 +109,7 @@ public override IMemoryOwner Allocate(
return buffer;
}
- if (lengthInBytes <= this.poolBufferSizeInBytes)
+ if (lengthInBytes <= (ulong)this.poolBufferSizeInBytes)
{
UnmanagedMemoryHandle mem = this.pool.Rent();
if (mem.IsValid)
@@ -115,15 +123,15 @@ public override IMemoryOwner Allocate(
}
///
- internal override MemoryGroup AllocateGroup(
- long totalLength,
+ internal override MemoryGroup AllocateGroupCore(
+ long totalLengthInElements,
+ long totalLengthInBytes,
int bufferAlignment,
AllocationOptions options = AllocationOptions.None)
{
- long totalLengthInBytes = totalLength * Unsafe.SizeOf();
if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes)
{
- var buffer = new SharedArrayPoolBuffer((int)totalLength);
+ var buffer = new SharedArrayPoolBuffer((int)totalLengthInElements);
return MemoryGroup.CreateContiguous(buffer, options.Has(AllocationOptions.Clean));
}
@@ -133,18 +141,18 @@ internal override MemoryGroup AllocateGroup(
UnmanagedMemoryHandle mem = this.pool.Rent();
if (mem.IsValid)
{
- UnmanagedBuffer buffer = this.pool.CreateGuardedBuffer(mem, (int)totalLength, options.Has(AllocationOptions.Clean));
+ UnmanagedBuffer buffer = this.pool.CreateGuardedBuffer(mem, (int)totalLengthInElements, options.Has(AllocationOptions.Clean));
return MemoryGroup.CreateContiguous(buffer, options.Has(AllocationOptions.Clean));
}
}
// Attempt to rent the whole group from the pool, allocate a group of unmanaged buffers if the attempt fails:
- if (MemoryGroup.TryAllocate(this.pool, totalLength, bufferAlignment, options, out MemoryGroup poolGroup))
+ if (MemoryGroup.TryAllocate(this.pool, totalLengthInElements, bufferAlignment, options, out MemoryGroup? poolGroup))
{
return poolGroup;
}
- return MemoryGroup.Allocate(this.nonPoolAllocator, totalLength, bufferAlignment, options);
+ return MemoryGroup.Allocate(this.nonPoolAllocator, totalLengthInElements, bufferAlignment, options);
}
public override void ReleaseRetainedResources() => this.pool.Release();
diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
index 9844f4a3bc..d77c2c4dc0 100644
--- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
+++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
@@ -20,7 +20,7 @@ namespace SixLabors.ImageSharp.Memory
/// The element type.
internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable
where T : struct
- {
+ {
private static readonly int ElementSize = Unsafe.SizeOf();
private MemoryGroupSpanCache memoryGroupSpanCache;
@@ -43,7 +43,7 @@ private MemoryGroup(int bufferLength, long totalLength)
///
public bool IsValid { get; private set; } = true;
- public MemoryGroupView View { get; private set; }
+ public MemoryGroupView View { get; private set; } = null!;
///
public abstract Memory this[int index] { get; }
@@ -85,12 +85,14 @@ public static MemoryGroup Allocate(
{
int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes();
Guard.NotNull(allocator, nameof(allocator));
- Guard.MustBeGreaterThanOrEqualTo(totalLengthInElements, 0, nameof(totalLengthInElements));
- Guard.MustBeGreaterThanOrEqualTo(bufferAlignmentInElements, 0, nameof(bufferAlignmentInElements));
- int blockCapacityInElements = bufferCapacityInBytes / ElementSize;
+ if (totalLengthInElements < 0)
+ {
+ throw new InvalidMemoryOperationException($"Attempted to allocate a buffer of negative length={totalLengthInElements}.");
+ }
- if (bufferAlignmentInElements > blockCapacityInElements)
+ int blockCapacityInElements = bufferCapacityInBytes / ElementSize;
+ if (bufferAlignmentInElements < 0 || bufferAlignmentInElements > blockCapacityInElements)
{
throw new InvalidMemoryOperationException(
$"The buffer capacity of the provided MemoryAllocator is insufficient for the requested buffer alignment: {bufferAlignmentInElements}.");
diff --git a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
index 92b1d8d359..e9730fb964 100644
--- a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
+++ b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs
@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0.
using System;
+using System.Diagnostics.CodeAnalysis;
namespace SixLabors.ImageSharp.Memory
{
diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
index 43ec45a34f..acc4c201b7 100644
--- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs
@@ -4,7 +4,7 @@
using System;
using System.IO;
using Microsoft.DotNet.RemoteExecutor;
-
+using Microsoft.DotNet.XUnitExtensions;
using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
@@ -619,5 +619,18 @@ public void BmpDecoder_CanDecode_Os2BitmapArray(TestImageProvider(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ // On V2 this is throwing InvalidOperationException,
+ // because of the validation logic in BmpInfoHeader.VerifyDimensions().
+ Assert.Throws(() =>
+ {
+ using Image image = provider.GetImage(BmpDecoder);
+ });
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
index 7a5241c5a8..124a2688a3 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
@@ -183,6 +183,17 @@ public void Issue1530_BadDescriptorDimensions(TestImageProvider
image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
}
+ // https://github.com/SixLabors/ImageSharp/issues/2758
+ [Theory]
+ [WithFile(TestImages.Gif.Issues.Issue2758, PixelTypes.Rgba32)]
+ public void Issue2758_BadDescriptorDimensions(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+ image.DebugSaveMultiFrame(provider);
+ image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
+ }
+
// https://github.com/SixLabors/ImageSharp/issues/405
[Theory]
[WithFile(TestImages.Gif.Issues.BadAppExtLength, PixelTypes.Rgba32)]
@@ -279,15 +290,9 @@ public void Issue2012EmptyXmp(TestImageProvider provider)
public void Issue2012BadMinCode(TestImageProvider provider)
where TPixel : unmanaged, IPixel
{
- Exception ex = Record.Exception(
- () =>
- {
- using Image image = provider.GetImage();
- image.DebugSave(provider);
- });
-
- Assert.NotNull(ex);
- Assert.Contains("Gif Image does not contain a valid LZW minimum code.", ex.Message);
+ using Image image = provider.GetImage();
+ image.DebugSave(provider);
+ image.CompareToReferenceOutput(provider);
}
// https://bugzilla.mozilla.org/show_bug.cgi?id=55918
@@ -301,5 +306,41 @@ public void IssueDeferredClearCode(TestImageProvider provider)
image.DebugSave(provider);
image.CompareFirstFrameToReferenceOutput(ImageComparer.Exact, provider);
}
+
+ // https://github.com/SixLabors/ImageSharp/issues/2743
+ [Theory]
+ [WithFile(TestImages.Gif.Issues.BadMaxLzwBits, PixelTypes.Rgba32)]
+ public void IssueTooLargeLzwBits(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+ image.DebugSaveMultiFrame(provider);
+ image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
+ }
+
+ // https://github.com/SixLabors/ImageSharp/issues/2859
+ [Theory]
+ [WithFile(TestImages.Gif.Issues.Issue2859_A, PixelTypes.Rgba32)]
+ [WithFile(TestImages.Gif.Issues.Issue2859_B, PixelTypes.Rgba32)]
+ public void Issue2859_LZWPixelStackOverflow(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+ image.DebugSaveMultiFrame(provider);
+ image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact);
+ }
+
+ // https://github.com/SixLabors/ImageSharp/issues/2953
+ [Theory]
+ [WithFile(TestImages.Gif.Issues.Issue2953, PixelTypes.Rgba32)]
+ public void Issue2953(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ // We should throw a InvalidImageContentException when trying to identify or load an invalid GIF file.
+ var testFile = TestFile.Create(provider.SourceFileOrDescription);
+
+ Assert.Throws(() => Image.Identify(testFile.FullPath));
+ Assert.Throws(() => Image.Load(testFile.FullPath));
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs
index efabed5b29..8bd9a96ae4 100644
--- a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs
@@ -190,5 +190,20 @@ public void Decode_VerifyRepeatCount(string imagePath, uint repeatCount)
}
}
}
+
+ [Theory]
+ [InlineData(TestImages.Gif.Issues.BadMaxLzwBits, 8)]
+ [InlineData(TestImages.Gif.Issues.Issue2012BadMinCode, 1)]
+ public void Identify_Frames_Bad_Lzw(string imagePath, int framesCount)
+ {
+ TestFile testFile = TestFile.Create(imagePath);
+ using MemoryStream stream = new(testFile.Bytes, false);
+
+ IImageInfo imageInfo = Image.Identify(stream);
+
+ Assert.NotNull(imageInfo);
+ GifMetadata gifMetadata = imageInfo.Metadata.GetGifMetadata();
+ Assert.NotNull(gifMetadata);
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
index d9915f17d6..d662a23bcc 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs
@@ -10,6 +10,7 @@
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
using Xunit;
@@ -364,6 +365,31 @@ public void EncodedStringTags_Read()
}
}
+ [Theory(Skip = "2.1 JPEG decoder detects this image as invalid.")]
+ [WithFile(TestImages.Jpeg.Issues.Issue2758, PixelTypes.L8)]
+ public void Issue2758_DecodeWorks(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage();
+
+ Assert.Equal(59787, image.Width);
+ Assert.Equal(511, image.Height);
+
+ JpegMetadata meta = image.Metadata.GetJpegMetadata();
+
+ // Quality determination should be between 1-100.
+ Assert.Equal(15, meta.LuminanceQuality);
+ Assert.Equal(1, meta.ChrominanceQuality);
+
+ // We want to test the encoder to ensure the determined values can be encoded but not by encoding
+ // the full size image as it would be too slow.
+ // We will crop the image to a smaller size and then encode it.
+ image.Mutate(x => x.Crop(new(0, 0, 100, 100)));
+
+ using MemoryStream ms = new();
+ image.Save(ms, new JpegEncoder());
+ }
+
private static void VerifyEncodedStrings(ExifProfile exif)
{
Assert.NotNull(exif);
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
index d12c840a1b..284e4fa254 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
@@ -229,6 +229,19 @@ public void Issue2133_DeduceColorSpace(TestImageProvider provide
}
}
+ // https://github.com/SixLabors/ImageSharp/issues/2638
+ [Theory]
+ [WithFile(TestImages.Jpeg.Issues.Issue2638, PixelTypes.Rgba32)]
+ public void Issue2638_DecodeWorks(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using (Image image = provider.GetImage(JpegDecoder))
+ {
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider);
+ }
+ }
+
// DEBUG ONLY!
// The PDF.js output should be saved by "tests\ImageSharp.Tests\Formats\Jpg\pdfjs\jpeg-converter.htm"
// into "\tests\Images\ActualOutput\JpegDecoderTests\"
@@ -261,5 +274,22 @@ public void ValidateProgressivePdfJsOutput(
this.Output.WriteLine($"Difference for PORT: {portReport.DifferencePercentageString}");
}
}
+
+ [Theory]
+ [WithFile(TestImages.Jpeg.Issues.HangBadScan, PixelTypes.L8)]
+ public void DecodeHang(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ if (TestEnvironment.IsWindows &&
+ TestEnvironment.RunsOnCI)
+ {
+ // Windows CI runs consistently fail with OOM.
+ return;
+ }
+
+ using Image image = provider.GetImage(JpegDecoder);
+ Assert.Equal(65503, image.Width);
+ Assert.Equal(65503, image.Height);
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
index eb3bc8c9a5..8708320e09 100644
--- a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
@@ -2,8 +2,10 @@
// Licensed under the Apache License, Version 2.0.
using System.IO;
+using System.Text;
using SixLabors.ImageSharp.Formats.Pbm;
using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Tests.TestUtilities;
using Xunit;
using static SixLabors.ImageSharp.Tests.TestImages.Pbm;
@@ -97,5 +99,25 @@ public void DecodeReferenceImage(TestImageProvider provider, str
bool isGrayscale = extension is "pgm" or "pbm";
image.CompareToReferenceOutput(provider, grayscale: isGrayscale);
}
+
+
+ [Fact]
+ public void PlainText_PrematureEof()
+ {
+ byte[] bytes = Encoding.ASCII.GetBytes($"P1\n100 100\n1 0 1 0 1 0");
+ using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(bytes);
+
+ Assert.True(eofHitCounter.EofHitCount <= 2);
+ Assert.Equal(new Size(100, 100), eofHitCounter.Image.Size());
+ }
+
+ [Fact]
+ public void Binary_PrematureEof()
+ {
+ using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(RgbBinaryPrematureEof);
+
+ Assert.True(eofHitCounter.EofHitCount <= 2);
+ Assert.Equal(new Size(29, 30), eofHitCounter.Image.Size());
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs
index 7915d224a9..8b5381c7ea 100644
--- a/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs
@@ -1,7 +1,9 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using System;
using System.IO;
+using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Pbm;
using Xunit;
@@ -82,5 +84,12 @@ public void Identify_DetectsCorrectComponentType(string imagePath, PbmComponentT
Assert.NotNull(bitmapMetadata);
Assert.Equal(expectedComponentType, bitmapMetadata.ComponentType);
}
+
+ [Fact]
+ public void Identify_EofInHeader_ThrowsInvalidImageContentException()
+ {
+ byte[] bytes = Convert.FromBase64String("UDEjWAAACQAAAAA=");
+ Assert.Throws(() => Image.Identify(bytes));
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
index a4fcf63baf..c361b1deb4 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
@@ -526,5 +526,13 @@ static void RunTest(string providerDump, string nonContiguousBuffersStr)
"Disco")
.Dispose();
}
+
+ [Theory]
+ [InlineData(TestImages.Png.Bad.Issue2714BadPalette)]
+ public void Decode_BadPalette(string file)
+ {
+ string path = Path.GetFullPath(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, file));
+ using Image image = Image.Load(path);
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
index 2a8cbcbf78..a4243c94b6 100644
--- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
@@ -668,6 +668,18 @@ public void TiffDecoder_ThrowsException_WithTooManyDirectories(TestImage
}
});
+ [Theory]
+ [WithFile(JpegCompressedGray0000539558, PixelTypes.Rgba32)]
+ public void TiffDecoder_ThrowsException_WithCircular_IFD_Offsets(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ => Assert.Throws(
+ () =>
+ {
+ using (provider.GetImage(TiffDecoder))
+ {
+ }
+ });
+
[Theory]
[WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)]
public void DecodeMultiframe(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
index f46c9519ca..7585998a64 100644
--- a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
+++ b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using System.Numerics;
+using System.Runtime.CompilerServices;
using System.Threading;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
@@ -410,6 +411,41 @@ void RowAction(RowInterval rows, Span memory)
Assert.Contains(width <= 0 ? "Width" : "Height", ex.Message);
}
+ [Fact]
+ public void CanIterateWithoutIntOverflow()
+ {
+ ParallelExecutionSettings parallelSettings = ParallelExecutionSettings.FromConfiguration(Configuration.Default);
+ const int max = 100_000;
+
+ Rectangle rect = new(0, 0, max, max);
+ int intervalMaxY = 0;
+ void RowAction(RowInterval rows, Span memory) => intervalMaxY = Math.Max(rows.Max, intervalMaxY);
+
+ TestRowOperation operation = new(0);
+ TestRowIntervalOperation intervalOperation = new(RowAction);
+
+ ParallelRowIterator.IterateRows(Configuration.Default, rect, in operation);
+ Assert.Equal(max - 1, operation.MaxY.Value);
+
+ ParallelRowIterator.IterateRowIntervals, Rgba32>(rect, in parallelSettings, in intervalOperation);
+ Assert.Equal(max, intervalMaxY);
+ }
+
+ private readonly struct TestRowOperation : IRowOperation
+ {
+ public TestRowOperation(int _) => this.MaxY = new StrongBox();
+
+ public StrongBox MaxY { get; }
+
+ public void Invoke(int y)
+ {
+ lock (this.MaxY)
+ {
+ this.MaxY.Value = Math.Max(y, this.MaxY.Value);
+ }
+ }
+ }
+
private readonly struct TestRowIntervalOperation : IRowIntervalOperation
{
private readonly Action action;
diff --git a/tests/ImageSharp.Tests/Image/ImageTests.cs b/tests/ImageSharp.Tests/Image/ImageTests.cs
index 0a9e2817a5..1bfd307cbd 100644
--- a/tests/ImageSharp.Tests/Image/ImageTests.cs
+++ b/tests/ImageSharp.Tests/Image/ImageTests.cs
@@ -40,6 +40,10 @@ public void Width_Height()
}
}
+ [Fact]
+ public void Width_Height_SizeNotRepresentable_ThrowsInvalidImageOperationException()
+ => Assert.Throws(() => new Image(int.MaxValue, int.MaxValue));
+
[Fact]
public void Configuration_Width_Height()
{
diff --git a/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs
index 3ab45be82d..c4ff74cdd2 100644
--- a/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs
+++ b/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs
@@ -21,13 +21,17 @@ public BufferTests()
protected SimpleGcMemoryAllocator MemoryAllocator { get; } = new SimpleGcMemoryAllocator();
- [Theory]
- [InlineData(-1)]
- public void Allocate_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length)
+ public static TheoryData InvalidLengths { get; set; } = new()
{
- ArgumentOutOfRangeException ex = Assert.Throws(() => this.MemoryAllocator.Allocate(length));
- Assert.Equal("length", ex.ParamName);
- }
+ { -1 },
+ { (1 << 30) + 1 }
+ };
+
+ [Theory]
+ [MemberData(nameof(InvalidLengths))]
+ public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException(int length)
+ => Assert.Throws(
+ () => this.MemoryAllocator.Allocate(length));
[Fact]
public unsafe void Allocate_MemoryIsPinnableMultipleTimes()
diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
index 0520c3c1fe..ff2d13b9de 100644
--- a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
+++ b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
@@ -111,6 +111,24 @@ public void AllocateGroup_MultipleTimes_ExceedPoolLimit()
}
}
+ [Fact]
+ public void AllocateGroup_SizeInBytesOverLongMaxValue_ThrowsInvalidMemoryOperationException()
+ {
+ var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(null);
+ Assert.Throws(() => allocator.AllocateGroup(int.MaxValue * (long)int.MaxValue, int.MaxValue));
+ }
+
+ public static TheoryData InvalidLengths { get; set; } = new()
+ {
+ { -1 },
+ { (1 << 30) + 1 }
+ };
+
+ [Theory]
+ [MemberData(nameof(InvalidLengths))]
+ public void Allocate_IncorrectAmount_ThrowsCorrect_InvalidMemoryOperationException(int length)
+ => Assert.Throws(() => new UniformUnmanagedMemoryPoolMemoryAllocator(null).Allocate(length));
+
[Fact]
public unsafe void Allocate_MemoryIsPinnableMultipleTimes()
{
@@ -380,6 +398,30 @@ private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllo
}
}
+ [Fact]
+ public void Allocate_OverLimit_ThrowsInvalidMemoryOperationException()
+ {
+ MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions()
+ {
+ AllocationLimitMegabytes = 4
+ });
+ const int oneMb = 1 << 20;
+ allocator.Allocate(4 * oneMb).Dispose(); // Should work
+ Assert.Throws(() => allocator.Allocate(5 * oneMb));
+ }
+
+ [Fact]
+ public void AllocateGroup_OverLimit_ThrowsInvalidMemoryOperationException()
+ {
+ MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions()
+ {
+ AllocationLimitMegabytes = 4
+ });
+ const int oneMb = 1 << 20;
+ allocator.AllocateGroup(4 * oneMb, 1024).Dispose(); // Should work
+ Assert.Throws(() => allocator.AllocateGroup(5 * oneMb, 1024));
+ }
+
#if NETCOREAPP3_1_OR_GREATER
[Fact]
public void Issue2001_NegativeMemoryReportedByGc()
@@ -394,5 +436,21 @@ static void RunTest()
}
}
#endif
+
+ [ConditionalFact(typeof(Environment), nameof(Environment.Is64BitProcess))]
+ public void MemoryAllocator_Create_SetHighLimit()
+ {
+ RemoteExecutor.Invoke(RunTest).Dispose();
+ static void RunTest()
+ {
+ const long threeGB = 3L * (1 << 30);
+ MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions()
+ {
+ AllocationLimitMegabytes = (int)(threeGB / 1024)
+ });
+ using MemoryGroup