diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs
index 56593acb84..9b28a8fdd8 100644
--- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs
+++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor.cs
@@ -22,10 +22,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
bool clipHistogram,
int clipLimit,
int numberOfTiles)
- : base(luminanceLevels, clipHistogram, clipLimit)
- {
- this.NumberOfTiles = numberOfTiles;
- }
+ : base(luminanceLevels, clipHistogram, clipLimit) => this.NumberOfTiles = numberOfTiles;
///
/// Gets the number of tiles the image is split into (horizontal and vertically) for the adaptive histogram equalization.
@@ -34,8 +31,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
///
public override IImageProcessor CreatePixelSpecificProcessor(Configuration configuration, Image source, Rectangle sourceRectangle)
- {
- return new AdaptiveHistogramEqualizationProcessor(
+ => new AdaptiveHistogramEqualizationProcessor(
configuration,
this.LuminanceLevels,
this.ClipHistogram,
@@ -43,6 +39,5 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
this.NumberOfTiles,
source,
sourceRectangle);
- }
}
}
diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs
index 14687426d0..91ed9f5de4 100644
--- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs
@@ -459,10 +459,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
private readonly Configuration configuration;
private readonly MemoryAllocator memoryAllocator;
- // Used for storing the minimum value for each CDF entry.
+ ///
+ /// Used for storing the minimum value for each CDF entry.
+ ///
private readonly Buffer2D cdfMinBuffer2D;
- // Used for storing the LUT for each CDF entry.
+ ///
+ /// Used for storing the LUT for each CDF entry.
+ ///
private readonly Buffer2D cdfLutBuffer2D;
private readonly int pixelsInTile;
private readonly int sourceWidth;
@@ -596,6 +600,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
int y = this.tileYStartPositions[index].y;
int endY = Math.Min(y + this.tileHeight, this.sourceHeight);
Span cdfMinSpan = this.cdfMinBuffer2D.GetRowSpan(cdfY);
+ cdfMinSpan.Clear();
using IMemoryOwner histogramBuffer = this.allocator.Allocate(this.luminanceLevels);
Span histogram = histogramBuffer.GetSpan();
diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
index 60686f4014..f93334beb0 100644
--- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
+++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor.cs
@@ -49,44 +49,18 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
///
/// The .
/// The .
- public static HistogramEqualizationProcessor FromOptions(HistogramEqualizationOptions options)
+ public static HistogramEqualizationProcessor FromOptions(HistogramEqualizationOptions options) => options.Method switch
{
- HistogramEqualizationProcessor processor;
+ HistogramEqualizationMethod.Global
+ => new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit),
- switch (options.Method)
- {
- case HistogramEqualizationMethod.Global:
- processor = new GlobalHistogramEqualizationProcessor(
- options.LuminanceLevels,
- options.ClipHistogram,
- options.ClipLimit);
- break;
+ HistogramEqualizationMethod.AdaptiveTileInterpolation
+ => new AdaptiveHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.NumberOfTiles),
- case HistogramEqualizationMethod.AdaptiveTileInterpolation:
- processor = new AdaptiveHistogramEqualizationProcessor(
- options.LuminanceLevels,
- options.ClipHistogram,
- options.ClipLimit,
- options.NumberOfTiles);
- break;
+ HistogramEqualizationMethod.AdaptiveSlidingWindow
+ => new AdaptiveHistogramEqualizationSlidingWindowProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit, options.NumberOfTiles),
- case HistogramEqualizationMethod.AdaptiveSlidingWindow:
- processor = new AdaptiveHistogramEqualizationSlidingWindowProcessor(
- options.LuminanceLevels,
- options.ClipHistogram,
- options.ClipLimit,
- options.NumberOfTiles);
- break;
-
- default:
- processor = new GlobalHistogramEqualizationProcessor(
- options.LuminanceLevels,
- options.ClipHistogram,
- options.ClipLimit);
- break;
- }
-
- return processor;
- }
+ _ => new GlobalHistogramEqualizationProcessor(options.LuminanceLevels, options.ClipHistogram, options.ClipLimit),
+ };
}
}
diff --git a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs
index 59df3058d9..9227cb0c01 100644
--- a/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs
+++ b/src/ImageSharp/Processing/Processors/Normalization/HistogramEqualizationProcessor{TPixel}.cs
@@ -142,6 +142,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Normalization
[MethodImpl(InliningOptions.ShortMethod)]
public static int GetLuminance(TPixel sourcePixel, int luminanceLevels)
{
+ // TODO: We need a bulk per span equivalent.
var vector = sourcePixel.ToVector4();
return ColorNumerics.GetBT709Luminance(ref vector, luminanceLevels);
}
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
index 27d70fd188..67df6a8814 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs
@@ -12,6 +12,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils;
+using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
@@ -127,60 +128,53 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
}
[Theory]
- [InlineData(TestImages.Jpeg.Baseline.Jpeg420Small, 0)]
- [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 1)]
- [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 15)]
- [InlineData(TestImages.Jpeg.Issues.ExifGetString750Transform, 30)]
- [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 1)]
- [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 15)]
- [InlineData(TestImages.Jpeg.Issues.BadRstProgressive518, 30)]
- public async Task Decode_IsCancellable(string fileName, int cancellationDelayMs)
+ [InlineData(0)]
+ [InlineData(0.5)]
+ [InlineData(0.9)]
+ public async Task Decode_IsCancellable(int percentageOfStreamReadToCancel)
{
- // Decoding these huge files took 300ms on i7-8650U in 2020. 30ms should be safe for cancellation delay.
- string hugeFile = Path.Combine(
- TestEnvironment.InputImagesDirectoryFullPath,
- fileName);
-
- const int numberOfRuns = 5;
-
- for (int i = 0; i < numberOfRuns; i++)
+ var cts = new CancellationTokenSource();
+ var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
+ using var pausedStream = new PausedStream(file);
+ pausedStream.OnWaiting(s =>
{
- var cts = new CancellationTokenSource();
- if (cancellationDelayMs == 0)
+ if (s.Position >= s.Length * percentageOfStreamReadToCancel)
{
cts.Cancel();
+ pausedStream.Release();
}
else
{
- cts.CancelAfter(cancellationDelayMs);
- }
-
- try
- {
- using Image image = await Image.LoadAsync(hugeFile, cts.Token);
- }
- catch (TaskCanceledException)
- {
- // Successfully observed a cancellation
- return;
+ // allows this/next wait to unblock
+ pausedStream.Next();
}
- }
+ });
- throw new Exception($"No cancellation happened out of {numberOfRuns} runs!");
+ var config = Configuration.CreateDefaultInstance();
+ config.FileSystem = new SingleStreamFileSystem(pausedStream);
+ await Assert.ThrowsAsync(async () =>
+ {
+ using Image image = await Image.LoadAsync(config, "someFakeFile", cts.Token);
+ });
}
- [Theory(Skip = "Identify is too fast, doesn't work reliably.")]
- [InlineData(TestImages.Jpeg.Baseline.Exif)]
- [InlineData(TestImages.Jpeg.Progressive.Bad.ExifUndefType)]
- public async Task Identify_IsCancellable(string fileName)
+ [Fact]
+ public async Task Identify_IsCancellable()
{
- string file = Path.Combine(
- TestEnvironment.InputImagesDirectoryFullPath,
- fileName);
-
var cts = new CancellationTokenSource();
- cts.CancelAfter(TimeSpan.FromTicks(1));
- await Assert.ThrowsAsync(() => Image.IdentifyAsync(file, cts.Token));
+
+ var file = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Jpeg.Baseline.Jpeg420Small);
+ using var pausedStream = new PausedStream(file);
+ pausedStream.OnWaiting(s =>
+ {
+ cts.Cancel();
+ pausedStream.Release();
+ });
+
+ var config = Configuration.CreateDefaultInstance();
+ config.FileSystem = new SingleStreamFileSystem(pausedStream);
+
+ await Assert.ThrowsAsync(async () => await Image.IdentifyAsync(config, "someFakeFile", cts.Token));
}
// DEBUG ONLY!
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
index 9a1d423a6d..3c48865c71 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
@@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
+using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
@@ -310,28 +311,33 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
}
[Theory]
- [InlineData(JpegSubsample.Ratio420, 0)]
- [InlineData(JpegSubsample.Ratio420, 3)]
- [InlineData(JpegSubsample.Ratio420, 10)]
- [InlineData(JpegSubsample.Ratio444, 0)]
- [InlineData(JpegSubsample.Ratio444, 3)]
- [InlineData(JpegSubsample.Ratio444, 10)]
- public async Task Encode_IsCancellable(JpegSubsample subsample, int cancellationDelayMs)
+ [InlineData(JpegSubsample.Ratio420)]
+ [InlineData(JpegSubsample.Ratio444)]
+ public async Task Encode_IsCancellable(JpegSubsample subsample)
{
- using var image = new Image(5000, 5000);
- using var stream = new MemoryStream();
var cts = new CancellationTokenSource();
- if (cancellationDelayMs == 0)
- {
- cts.Cancel();
- }
- else
+ using var pausedStream = new PausedStream(new MemoryStream());
+ pausedStream.OnWaiting(s =>
{
- cts.CancelAfter(cancellationDelayMs);
- }
+ // after some writing
+ if (s.Position >= 500)
+ {
+ cts.Cancel();
+ pausedStream.Release();
+ }
+ else
+ {
+ // allows this/next wait to unblock
+ pausedStream.Next();
+ }
+ });
- var encoder = new JpegEncoder() { Subsample = subsample };
- await Assert.ThrowsAsync(() => image.SaveAsync(stream, encoder, cts.Token));
+ using var image = new Image(5000, 5000);
+ await Assert.ThrowsAsync(async () =>
+ {
+ var encoder = new JpegEncoder() { Subsample = subsample };
+ await image.SaveAsync(pausedStream, encoder, cts.Token);
+ });
}
}
}
diff --git a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
index 825bd55e47..8bb121349f 100644
--- a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
+++ b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
@@ -140,10 +140,15 @@ namespace SixLabors.ImageSharp.Tests
using var stream = new MemoryStream();
var asyncStream = new AsyncStreamWrapper(stream, () => false);
var cts = new CancellationTokenSource();
- cts.CancelAfter(TimeSpan.FromTicks(1));
- await Assert.ThrowsAnyAsync(() =>
- image.SaveAsync(asyncStream, encoder, cts.Token));
+ var pausedStream = new PausedStream(asyncStream);
+ pausedStream.OnWaiting(s =>
+ {
+ cts.Cancel();
+ pausedStream.Release();
+ });
+
+ await Assert.ThrowsAsync(async () => await image.SaveAsync(pausedStream, encoder, cts.Token));
}
}
}
diff --git a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
index ab3a1d7603..85b7530247 100644
--- a/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
+++ b/tests/ImageSharp.Tests/Processing/Normalization/HistogramEqualizationTests.cs
@@ -141,6 +141,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
/// See: https://github.com/SixLabors/ImageSharp/pull/984
///
/// The pixel type of the image.
+ /// The test image provider.
[Theory]
[WithTestPatternImages(110, 110, PixelTypes.Rgb24)]
[WithTestPatternImages(170, 170, PixelTypes.Rgb24)]
@@ -162,5 +163,43 @@ namespace SixLabors.ImageSharp.Tests.Processing.Normalization
image.CompareToReferenceOutput(ValidatorComparer, provider);
}
}
+
+ [Theory]
+ [WithTestPatternImages(5120, 9234, PixelTypes.L16)]
+ public unsafe void Issue1640(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ if (!TestEnvironment.Is64BitProcess)
+ {
+ return;
+ }
+
+ // https://github.com/SixLabors/ImageSharp/discussions/1640
+ // Test using isolated memory to ensure clean buffers for reference
+ provider.Configuration = Configuration.CreateDefaultInstance();
+ var options = new HistogramEqualizationOptions
+ {
+ Method = HistogramEqualizationMethod.AdaptiveTileInterpolation,
+ LuminanceLevels = 4096,
+ ClipHistogram = false,
+ ClipLimit = 350,
+ NumberOfTiles = 8
+ };
+
+ using Image image = provider.GetImage();
+ using Image referenceResult = image.Clone(ctx =>
+ {
+ ctx.HistogramEqualization(options);
+ ctx.Resize(image.Width / 4, image.Height / 4, KnownResamplers.Bicubic);
+ });
+
+ using Image processed = image.Clone(ctx =>
+ {
+ ctx.HistogramEqualization(options);
+ ctx.Resize(image.Width / 4, image.Height / 4, KnownResamplers.Bicubic);
+ });
+
+ ValidatorComparer.VerifySimilarity(referenceResult, processed);
+ }
}
}
diff --git a/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs
new file mode 100644
index 0000000000..4d3646301f
--- /dev/null
+++ b/tests/ImageSharp.Tests/TestUtilities/PausedStream.cs
@@ -0,0 +1,145 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace SixLabors.ImageSharp.Tests.TestUtilities
+{
+ public class PausedStream : Stream
+ {
+ private readonly SemaphoreSlim semaphore = new SemaphoreSlim(0);
+
+ private readonly CancellationTokenSource cancelationTokenSource = new CancellationTokenSource();
+
+ private readonly Stream innerStream;
+ private Action onWaitingCallback;
+
+ public void OnWaiting(Action onWaitingCallback) => this.onWaitingCallback = onWaitingCallback;
+
+ public void OnWaiting(Action onWaitingCallback) => this.OnWaiting(_ => onWaitingCallback());
+
+ public void Release()
+ {
+ this.semaphore.Release();
+ this.cancelationTokenSource.Cancel();
+ }
+
+ public void Next() => this.semaphore.Release();
+
+ private void Wait()
+ {
+ if (this.cancelationTokenSource.IsCancellationRequested)
+ {
+ return;
+ }
+
+ this.onWaitingCallback?.Invoke(this.innerStream);
+
+ try
+ {
+ this.semaphore.Wait(this.cancelationTokenSource.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ // ignore this as its just used to unlock any waits in progress
+ }
+ }
+
+ private async Task Await(Func action)
+ {
+ await Task.Yield();
+ this.Wait();
+ await action();
+ }
+
+ private async Task Await(Func> action)
+ {
+ await Task.Yield();
+ this.Wait();
+ return await action();
+ }
+
+ private T Await(Func action)
+ {
+ this.Wait();
+ return action();
+ }
+
+ private void Await(Action action)
+ {
+ this.Wait();
+ action();
+ }
+
+ public PausedStream(byte[] data)
+ : this(new MemoryStream(data))
+ {
+ }
+
+ public PausedStream(string filePath)
+ : this(File.OpenRead(filePath))
+ {
+ }
+
+ public PausedStream(Stream innerStream) => this.innerStream = innerStream;
+
+ public override bool CanTimeout => this.innerStream.CanTimeout;
+
+ public override void Close() => this.Await(() => this.innerStream.Close());
+
+ public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) => this.Await(() => this.innerStream.CopyToAsync(destination, bufferSize, cancellationToken));
+
+ public override bool CanRead => this.innerStream.CanRead;
+
+ public override bool CanSeek => this.innerStream.CanSeek;
+
+ public override bool CanWrite => this.innerStream.CanWrite;
+
+ public override long Length => this.Await(() => this.innerStream.Length);
+
+ public override long Position { get => this.Await(() => this.innerStream.Position); set => this.Await(() => this.innerStream.Position = value); }
+
+ public override void Flush() => this.Await(() => this.innerStream.Flush());
+
+ public override int Read(byte[] buffer, int offset, int count) => this.Await(() => this.innerStream.Read(buffer, offset, count));
+
+ public override long Seek(long offset, SeekOrigin origin) => this.Await(() => this.innerStream.Seek(offset, origin));
+
+ public override void SetLength(long value) => this.Await(() => this.innerStream.SetLength(value));
+
+ public override void Write(byte[] buffer, int offset, int count) => this.Await(() => this.innerStream.Write(buffer, offset, count));
+
+ public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.ReadAsync(buffer, offset, count, cancellationToken));
+
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => this.Await(() => this.innerStream.WriteAsync(buffer, offset, count, cancellationToken));
+
+ public override void WriteByte(byte value) => this.Await(() => this.innerStream.WriteByte(value));
+
+ public override int ReadByte() => this.Await(() => this.innerStream.ReadByte());
+
+ protected override void Dispose(bool disposing) => this.innerStream.Dispose();
+
+#if NETCOREAPP
+ public override void CopyTo(Stream destination, int bufferSize) => this.Await(() => this.innerStream.CopyTo(destination, bufferSize));
+
+ public override int Read(Span buffer)
+ {
+ this.Wait();
+ return this.innerStream.Read(buffer);
+ }
+
+ public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.ReadAsync(buffer, cancellationToken));
+
+ public override void Write(ReadOnlySpan buffer)
+ {
+ this.Wait();
+ this.innerStream.Write(buffer);
+ }
+
+ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) => this.Await(() => this.innerStream.WriteAsync(buffer, cancellationToken));
+#endif
+ }
+}
diff --git a/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs b/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs
new file mode 100644
index 0000000000..ddd1ec7506
--- /dev/null
+++ b/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+
+using System.IO;
+using SixLabors.ImageSharp.IO;
+
+namespace SixLabors.ImageSharp.Tests.TestUtilities
+{
+ internal class SingleStreamFileSystem : IFileSystem
+ {
+ private readonly Stream stream;
+
+ public SingleStreamFileSystem(Stream stream) => this.stream = stream;
+
+ Stream IFileSystem.Create(string path) => this.stream;
+
+ Stream IFileSystem.OpenRead(string path) => this.stream;
+ }
+}