Skip to content

Commit

Permalink
Throw ObjectDisposedException when trying to operate on a disposed im…
Browse files Browse the repository at this point in the history
…age (#968)

* disable multitargeting + TreatWarningsAsErrors to for fast development

* Check if image is disposed

in significant Image and Image<T> methods

* Mutate / Clone: ensure image is not disposed

* Revert "disable multitargeting + TreatWarningsAsErrors to for fast development"

This reverts commit 9ad74f7.
  • Loading branch information
antonfirsov authored and JimBobSquarePants committed Aug 12, 2019
1 parent 6f11341 commit 2909994
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 10 deletions.
22 changes: 21 additions & 1 deletion src/ImageSharp/Image.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,22 @@ internal Image(
/// </summary>
Configuration IConfigurable.Configuration => this.Configuration;

/// <summary>
/// Gets a value indicating whether the image instance is disposed.
/// </summary>
public bool IsDisposed { get; private set; }

/// <inheritdoc />
public abstract void Dispose();
public void Dispose()
{
if (this.IsDisposed)
{
return;
}

this.IsDisposed = true;
this.DisposeImpl();
}

/// <summary>
/// Saves the image to the given stream using the given image encoder.
Expand All @@ -93,6 +107,7 @@ public void Save(Stream stream, IImageEncoder encoder)
{
Guard.NotNull(stream, nameof(stream));
Guard.NotNull(encoder, nameof(encoder));
this.EnsureNotDisposed();

EncodeVisitor visitor = new EncodeVisitor(encoder, stream);
this.AcceptVisitor(visitor);
Expand Down Expand Up @@ -128,6 +143,11 @@ public abstract Image<TPixel2> CloneAs<TPixel2>(Configuration configuration)
/// <param name="size">The <see cref="Size"/>.</param>
protected void UpdateSize(Size size) => this.size = size;

/// <summary>
/// Implements the Dispose logic.
/// </summary>
protected abstract void DisposeImpl();

private class EncodeVisitor : IImageVisitor
{
private readonly IImageEncoder encoder;
Expand Down
13 changes: 12 additions & 1 deletion src/ImageSharp/ImageExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,5 +119,16 @@ public static string ToBase64String<TPixel>(this Image<TPixel> source, IImageFor
return $"data:{format.DefaultMimeType};base64,{Convert.ToBase64String(stream.ToArray())}";
}
}

/// <summary>
/// Throws <see cref="ObjectDisposedException"/> if the image is disposed.
/// </summary>
internal static void EnsureNotDisposed(this Image image)
{
if (image.IsDisposed)
{
throw new ObjectDisposedException(nameof(image), "Trying to execute an operation on a disposed image.");
}
}
}
}
}
8 changes: 7 additions & 1 deletion src/ImageSharp/Image{TPixel}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable<
/// <returns>Returns a new <see cref="Image{TPixel}"/> with all the same pixel data as the original.</returns>
public Image<TPixel> Clone(Configuration configuration)
{
this.EnsureNotDisposed();

IEnumerable<ImageFrame<TPixel>> clonedFrames =
this.Frames.Select<ImageFrame<TPixel>, ImageFrame<TPixel>>(x => x.Clone(configuration));
return new Image<TPixel>(configuration, this.Metadata.DeepClone(), clonedFrames);
Expand All @@ -175,17 +177,21 @@ public Image<TPixel> Clone(Configuration configuration)
/// <returns>The <see cref="Image{TPixel2}"/>.</returns>
public override Image<TPixel2> CloneAs<TPixel2>(Configuration configuration)
{
this.EnsureNotDisposed();

IEnumerable<ImageFrame<TPixel2>> clonedFrames =
this.Frames.Select<ImageFrame<TPixel>, ImageFrame<TPixel2>>(x => x.CloneAs<TPixel2>(configuration));
return new Image<TPixel2>(configuration, this.Metadata.DeepClone(), clonedFrames);
}

/// <inheritdoc/>
public override void Dispose() => this.Frames.Dispose();
protected override void DisposeImpl() => this.Frames.Dispose();

/// <inheritdoc />
internal override void AcceptVisitor(IImageVisitor visitor)
{
this.EnsureNotDisposed();

visitor.Visit(this);
}

Expand Down
22 changes: 17 additions & 5 deletions src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ public static class ProcessingExtensions
/// <param name="operation">The operation to perform on the source.</param>
public static void Mutate(this Image source, Action<IImageProcessingContext> operation)
{
Guard.NotNull(source, nameof(source));
Guard.NotNull(operation, nameof(operation));
source.EnsureNotDisposed();

ProcessingVisitor visitor = new ProcessingVisitor(operation, true);
source.AcceptVisitor(visitor);
}
Expand All @@ -34,8 +38,9 @@ public static void Mutate(this Image source, Action<IImageProcessingContext> ope
public static void Mutate<TPixel>(this Image<TPixel> source, Action<IImageProcessingContext> operation)
where TPixel : struct, IPixel<TPixel>
{
Guard.NotNull(operation, nameof(operation));
Guard.NotNull(source, nameof(source));
Guard.NotNull(operation, nameof(operation));
source.EnsureNotDisposed();

IInternalImageProcessingContext<TPixel> operationsRunner = source.GetConfiguration().ImageOperationsProvider
.CreateImageProcessingContext(source, true);
Expand All @@ -51,8 +56,9 @@ public static void Mutate<TPixel>(this Image<TPixel> source, Action<IImageProces
public static void Mutate<TPixel>(this Image<TPixel> source, params IImageProcessor[] operations)
where TPixel : struct, IPixel<TPixel>
{
Guard.NotNull(operations, nameof(operations));
Guard.NotNull(source, nameof(source));
Guard.NotNull(operations, nameof(operations));
source.EnsureNotDisposed();

IInternalImageProcessingContext<TPixel> operationsRunner = source.GetConfiguration().ImageOperationsProvider
.CreateImageProcessingContext(source, true);
Expand All @@ -67,6 +73,10 @@ public static void Mutate<TPixel>(this Image<TPixel> source, params IImageProces
/// <returns>The new <see cref="SixLabors.ImageSharp.Image"/>.</returns>
public static Image Clone(this Image source, Action<IImageProcessingContext> operation)
{
Guard.NotNull(source, nameof(source));
Guard.NotNull(operation, nameof(operation));
source.EnsureNotDisposed();

ProcessingVisitor visitor = new ProcessingVisitor(operation, false);
source.AcceptVisitor(visitor);
return visitor.ResultImage;
Expand All @@ -82,8 +92,9 @@ public static Image Clone(this Image source, Action<IImageProcessingContext> ope
public static Image<TPixel> Clone<TPixel>(this Image<TPixel> source, Action<IImageProcessingContext> operation)
where TPixel : struct, IPixel<TPixel>
{
Guard.NotNull(operation, nameof(operation));
Guard.NotNull(source, nameof(source));
Guard.NotNull(operation, nameof(operation));
source.EnsureNotDisposed();

IInternalImageProcessingContext<TPixel> operationsRunner = source.GetConfiguration().ImageOperationsProvider
.CreateImageProcessingContext(source, false);
Expand All @@ -101,8 +112,9 @@ public static Image<TPixel> Clone<TPixel>(this Image<TPixel> source, Action<IIma
public static Image<TPixel> Clone<TPixel>(this Image<TPixel> source, params IImageProcessor[] operations)
where TPixel : struct, IPixel<TPixel>
{
Guard.NotNull(operations, nameof(operations));
Guard.NotNull(source, nameof(source));
Guard.NotNull(operations, nameof(operations));
source.EnsureNotDisposed();

IInternalImageProcessingContext<TPixel> operationsRunner = source.GetConfiguration().ImageOperationsProvider
.CreateImageProcessingContext(source, false);
Expand Down Expand Up @@ -152,4 +164,4 @@ public void Visit<TPixel>(Image<TPixel> image)
}
}
}
}
}
20 changes: 19 additions & 1 deletion tests/ImageSharp.Tests/Image/ImageCloneTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ namespace SixLabors.ImageSharp.Tests
{
public class ImageCloneTests
{
[Fact]
public void CloneAs_WhenDisposed_Throws()
{
Image<Rgba32> image = new Image<Rgba32>(5, 5);
image.Dispose();

Assert.Throws<ObjectDisposedException>(() => image.CloneAs<Bgra32>());
}

[Fact]
public void Clone_WhenDisposed_Throws()
{
Image<Rgba32> image = new Image<Rgba32>(5, 5);
image.Dispose();

Assert.Throws<ObjectDisposedException>(() => image.Clone());
}

[Theory]
[WithTestPatternImages(9, 9, PixelTypes.Rgba32)]
public void CloneAs_ToBgra32(TestImageProvider<Rgba32> provider)
Expand Down Expand Up @@ -109,4 +127,4 @@ public void CloneAs_ToRgb24(TestImageProvider<Rgba32> provider)
}
}
}
}
}
16 changes: 16 additions & 0 deletions tests/ImageSharp.Tests/Image/ImageTests.Save.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
// ReSharper disable InconsistentNaming

using System;
using System.IO;

using Moq;

using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
Expand Down Expand Up @@ -65,6 +69,18 @@ public void SetEncoding()
Assert.Equal("image/png", mime.DefaultMimeType);
}
}

[Fact]
public void ThrowsWhenDisposed()
{
var image = new Image<Rgba32>(5, 5);
image.Dispose();
IImageEncoder encoder = Mock.Of<IImageEncoder>();
using (MemoryStream stream = new MemoryStream())
{
Assert.Throws<ObjectDisposedException>(() => image.Save(stream, encoder));
}
}
}
}
}
65 changes: 64 additions & 1 deletion tests/ImageSharp.Tests/ImageOperationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,22 @@ public class ImageOperationTests : IDisposable
private readonly FakeImageOperationsProvider provider;
private readonly IImageProcessor processorDefinition;

private static readonly string ExpectedExceptionMessage = GetExpectedExceptionText();

public ImageOperationTests()
{
this.provider = new FakeImageOperationsProvider();

Mock<IImageProcessor> processorMock = new Mock<IImageProcessor>();
this.processorDefinition = processorMock.Object;

this.image = new Image<Rgba32>(new Configuration
{
ImageOperationsProvider = this.provider
}, 1, 1);
}


[Fact]
public void MutateCallsImageOperationsProvider_Func_OriginalImage()
{
Expand Down Expand Up @@ -109,5 +112,65 @@ public void ApplyProcessors_ListOfProcessors_AppliesAllProcessorsToOperation()
}

public void Dispose() => this.image.Dispose();

[Fact]
public void GenericMutate_WhenDisposed_Throws()
{
this.image.Dispose();

CheckThrowsCorrectObjectDisposedException(
() => this.image.Mutate(x => x.ApplyProcessor(this.processorDefinition)));
}

[Fact]
public void GenericClone_WhenDisposed_Throws()
{
this.image.Dispose();

CheckThrowsCorrectObjectDisposedException(
() => this.image.Clone(x => x.ApplyProcessor(this.processorDefinition)));
}

[Fact]
public void AgnosticMutate_WhenDisposed_Throws()
{
this.image.Dispose();
Image img = this.image;

CheckThrowsCorrectObjectDisposedException(
() => img.Mutate(x => x.ApplyProcessor(this.processorDefinition)));
}

[Fact]
public void AgnosticClone_WhenDisposed_Throws()
{
this.image.Dispose();
Image img = this.image;

CheckThrowsCorrectObjectDisposedException(
() => img.Clone(x => x.ApplyProcessor(this.processorDefinition)));
}

private static string GetExpectedExceptionText()
{
try
{
Image<Rgba32> img = new Image<Rgba32>(1, 1);
img.Dispose();
img.EnsureNotDisposed();
}
catch (ObjectDisposedException ex)
{
return ex.Message;
}

return "?";
}

private static void CheckThrowsCorrectObjectDisposedException(Action action)
{
var ex = Assert.Throws<ObjectDisposedException>(action);
Assert.Equal(ExpectedExceptionMessage, ex.Message);
}
}
}

0 comments on commit 2909994

Please sign in to comment.