diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
index acc639eb7..593fe9277 100644
--- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
+++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
@@ -201,6 +201,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
{
Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream));
+ cancellationToken.ThrowIfCancellationRequested();
const ushort max = JpegConstants.MaxLength;
if (image.Width >= max || image.Height >= max)
@@ -249,7 +250,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.WriteDefineHuffmanTables(componentCount);
// Write the image data.
- this.WriteStartOfScan(image);
+ this.WriteStartOfScan(image, cancellationToken);
// Write the End Of Image marker.
this.buffer[0] = JpegConstants.Markers.XFF;
@@ -398,7 +399,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// The pixel format.
/// The pixel accessor providing access to the image pixels.
- private void Encode444(Image pixels)
+ /// The token to monitor for cancellation.
+ private void Encode444(Image pixels, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
// TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.)
@@ -420,6 +422,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
for (int y = 0; y < pixels.Height; y += 8)
{
+ cancellationToken.ThrowIfCancellationRequested();
var currentRows = new RowOctet(pixelBuffer, y);
for (int x = 0; x < pixels.Width; x += 8)
@@ -945,7 +948,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// The pixel format.
/// The pixel accessor providing access to the image pixels.
- private void WriteStartOfScan(Image image)
+ /// The token to monitor for cancellation.
+ private void WriteStartOfScan(Image image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
// TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.)
@@ -955,10 +959,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
switch (this.subsample)
{
case JpegSubsample.Ratio444:
- this.Encode444(image);
+ this.Encode444(image, cancellationToken);
break;
case JpegSubsample.Ratio420:
- this.Encode420(image);
+ this.Encode420(image, cancellationToken);
break;
}
@@ -972,7 +976,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
///
/// The pixel format.
/// The pixel accessor providing access to the image pixels.
- private void Encode420(Image pixels)
+ /// The token to monitor for cancellation.
+ private void Encode420(Image pixels, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel
{
// TODO: Need a JpegScanEncoder class or struct that encapsulates the scan-encoding implementation. (Similar to JpegScanDecoder.)
@@ -1000,6 +1005,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
for (int y = 0; y < pixels.Height; y += 16)
{
+ cancellationToken.ThrowIfCancellationRequested();
for (int x = 0; x < pixels.Width; x += 16)
{
for (int i = 0; i < 4; i++)
diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
index 6c9a74463..11d64e49f 100644
--- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
@@ -1,8 +1,11 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
+using System;
using System.Collections.Generic;
using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
@@ -284,5 +287,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
IccProfile values = input.Metadata.IccProfile;
Assert.Equal(values.Entries, actual.Entries);
}
+
+ [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)
+ {
+ using var image = new Image(5000, 5000);
+ using MemoryStream stream = new MemoryStream();
+ var cts = new CancellationTokenSource();
+ if (cancellationDelayMs == 0)
+ {
+ cts.Cancel();
+ }
+ else
+ {
+ cts.CancelAfter(cancellationDelayMs);
+ }
+
+ var encoder = new JpegEncoder() { Subsample = subsample };
+ await Assert.ThrowsAnyAsync(() => image.SaveAsync(stream, encoder, cts.Token));
+ }
}
}
diff --git a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
index 4a6c96ae8..de819570f 100644
--- a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
+++ b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs
@@ -3,9 +3,9 @@
using System;
using System.IO;
-
+using System.Threading;
using Moq;
-
+using SixLabors.ImageSharp.Formats.Bmp;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
@@ -90,7 +90,7 @@ namespace SixLabors.ImageSharp.Tests
[InlineData("test.bmp")]
[InlineData("test.jpg")]
[InlineData("test.gif")]
- public async Task SaveNeverCallsSyncMethods(string filename)
+ public async Task SaveAsync_NeverCallsSyncMethods(string filename)
{
using (var image = new Image(5, 5))
{
@@ -102,6 +102,20 @@ namespace SixLabors.ImageSharp.Tests
}
}
}
+
+ [Fact]
+ public async Task SaveAsync_WithNonSeekableStream_IsCancellable()
+ {
+ using var image = new Image(500, 500);
+ IImageEncoder encoder = new BmpEncoder();
+ 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));
+ }
}
}
}