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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 17 additions & 31 deletions ImageSharpCompare/ImageSharpCompare.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ public static bool ImagesAreEqual(Image actual, Image expected, ResizeOption res
Image<Rgb24>? expectedPixelAccusableImage = null;
try
{
actualPixelAccessibleImage = ToRgb24Image(actual, out ownsActual);
expectedPixelAccusableImage = ToRgb24Image(expected, out ownsExpected);
actualPixelAccessibleImage = ImageSharpPixelTypeConverter.ToRgb24Image(actual, out ownsActual);
expectedPixelAccusableImage = ImageSharpPixelTypeConverter.ToRgb24Image(expected, out ownsExpected);

return ImagesAreEqual(actualPixelAccessibleImage, expectedPixelAccusableImage, resizeOption);
}
Expand Down Expand Up @@ -286,8 +286,8 @@ public static ICompareResult CalcDiff(Image actual, Image expected, ResizeOption

try
{
actualRgb24 = ToRgb24Image(actual, out ownsActual);
expectedRgb24 = ToRgb24Image(expected, out ownsExpected);
actualRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(actual, out ownsActual);
expectedRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(expected, out ownsExpected);

return CalcDiff(actualRgb24, expectedRgb24, resizeOption, pixelColorShiftTolerance);
}
Expand Down Expand Up @@ -350,7 +350,7 @@ public static ICompareResult CalcDiff(Image<Rgb24> actual, Image<Rgb24> expected
var g = Math.Abs(expectedPixel.G - actualPixel.G);
var b = Math.Abs(expectedPixel.B - actualPixel.B);
var sum = r + g + b;
absoluteError = absoluteError + (sum > pixelColorShiftTolerance ? sum : 0);
absoluteError += (sum > pixelColorShiftTolerance ? sum : 0);
pixelErrorCount += (sum > pixelColorShiftTolerance) ? 1 : 0;
}
}
Expand Down Expand Up @@ -386,9 +386,9 @@ public static ICompareResult CalcDiff(Image actual, Image expected, Image maskIm

try
{
actualRgb24 = ToRgb24Image(actual, out ownsActual);
expectedRgb24 = ToRgb24Image(expected, out ownsExpected);
maskImageRgb24 = ToRgb24Image(maskImage, out ownsMask);
actualRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(actual, out ownsActual);
expectedRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(expected, out ownsExpected);
maskImageRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(maskImage, out ownsMask);

return CalcDiff(actualRgb24, expectedRgb24, maskImageRgb24, resizeOption, pixelColorShiftTolerance);
}
Expand Down Expand Up @@ -477,7 +477,7 @@ public static ICompareResult CalcDiff(Image<Rgb24> actual, Image<Rgb24> expected
error += b;
}

absoluteError = absoluteError + (error > pixelColorShiftTolerance ? error : 0);
absoluteError += (error > pixelColorShiftTolerance ? error : 0);
pixelErrorCount += error > pixelColorShiftTolerance ? 1 : 0;
}
}
Expand Down Expand Up @@ -571,8 +571,8 @@ public static Image CalcDiffMaskImage(Image actual, Image expected, ResizeOption

try
{
actualRgb24 = ToRgb24Image(actual, out ownsActual);
expectedRgb24 = ToRgb24Image(expected, out ownsExpected);
actualRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(actual, out ownsActual);
expectedRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(expected, out ownsExpected);

return CalcDiffMaskImage(actualRgb24, expectedRgb24, resizeOption, pixelColorShiftTolerance);
}
Expand Down Expand Up @@ -612,9 +612,9 @@ public static Image CalcDiffMaskImage(Image actual, Image expected, Image mask,

try
{
actualRgb24 = ToRgb24Image(actual, out ownsActual);
expectedRgb24 = ToRgb24Image(expected, out ownsExpected);
maskRgb24 = ToRgb24Image(mask, out ownsMask);
actualRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(actual, out ownsActual);
expectedRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(expected, out ownsExpected);
maskRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(mask, out ownsMask);

return CalcDiffMaskImage(actualRgb24, expectedRgb24, maskRgb24, resizeOption, pixelColorShiftTolerance);
}
Expand Down Expand Up @@ -744,27 +744,13 @@ public static Image CalcDiffMaskImage(Image<Rgb24> actual, Image<Rgb24> expected
}
}

private static Image<Rgb24> ToRgb24Image(Image actual, out bool ownsImage)
{
if (actual is Image<Rgb24> actualPixelAccessibleImage)
{
ownsImage = false;
return actualPixelAccessibleImage;
}

if (actual is Image<Rgba32> imageRgba32)
{
ownsImage = true;
return ConvertRgba32ToRgb24(imageRgba32);
}

throw new NotImplementedException($"Pixel type {actual.PixelType} is not supported to be compared.");
}

/// <summary>
/// Converts a Rgba32 Image to Rgb24 one
/// </summary>
/// <param name="imageRgba32"></param>
#pragma warning disable S1133 // Give the consumer of the public method some time to migrate
[Obsolete("use 'imageRgba32..CloneAs<Rgb24>()' instead")]
#pragma warning restore S1133
public static Image<Rgb24> ConvertRgba32ToRgb24(Image<Rgba32> imageRgba32)
{
ArgumentNullException.ThrowIfNull(imageRgba32);
Expand Down
2 changes: 1 addition & 1 deletion ImageSharpCompare/ImageSharpCompare.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="all" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.18.0.83559">
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.24.0.89429">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
Expand Down
31 changes: 31 additions & 0 deletions ImageSharpCompare/ImageSharpPixelTypeConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System;

namespace Codeuctivity.ImageSharpCompare
{
/// <summary>
/// Provides functionality to convert an ImageSharp image of any pixel type to an ImageSharp image with Rgb24 pixel type.
/// </summary>
public static class ImageSharpPixelTypeConverter
{
/// <summary>
/// Converts an Image with any pixel type to Rgb24
/// </summary>
/// <param name="image"></param>
/// <param name="isClonedInstance">Use this to dispose cloned instances</param>
public static Image<Rgb24> ToRgb24Image(Image image, out bool isClonedInstance)
{
ArgumentNullException.ThrowIfNull(image);

if (image is Image<Rgb24> actualPixelAccessibleImage)
{
isClonedInstance = false;
return actualPixelAccessibleImage;
}

isClonedInstance = true;
return image.CloneAs<Rgb24>();
}
}
}
28 changes: 28 additions & 0 deletions ImageSharpCompareTestNunit/AssertDisposeBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using NUnit.Framework;
using SixLabors.ImageSharp;
using System;
using System.Reflection;

namespace ImageSharpCompareTestNunit
{
internal static class AssertDisposeBehavior
{

internal static void AssertThatImageIsDisposed(Image image, bool expectedDisposeState = false)
{
const string imageSharpPrivateFieldNameIsDisposed = "isDisposed";
var isDisposed = (bool?)GetInstanceField(image, imageSharpPrivateFieldNameIsDisposed);
Assert.That(isDisposed, Is.EqualTo(expectedDisposeState));
image.Dispose();
isDisposed = (bool?)GetInstanceField(image, imageSharpPrivateFieldNameIsDisposed);
Assert.That(isDisposed, Is.True);
}

private static object? GetInstanceField<T>(T instance, string fieldName)
{
var bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
var field = typeof(T).GetField(fieldName, bindFlags);
return field == null ? throw new ArgumentNullException(fieldName) : field.GetValue(instance);
}
}
}
107 changes: 78 additions & 29 deletions ImageSharpCompareTestNunit/ImageSharpCompareTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.IO;
using System.Reflection;

namespace ImageSharpCompareTestNunit
{
Expand Down Expand Up @@ -121,8 +120,40 @@ public void ShouldVerifyThatImageSharpImagesAreEqual(string pathActual, string p
using var expected = Image.Load(absolutePathExpected);

Assert.That(ImageSharpCompare.ImagesAreEqual(actual, expected), Is.True);
AssertDisposeBehavior(actual);
AssertDisposeBehavior(expected);
AssertDisposeBehavior.AssertThatImageIsDisposed(actual);
AssertDisposeBehavior.AssertThatImageIsDisposed(expected);
}

[Test]
[TestCase(jpg0Rgb24, jpg0Rgb24)]
[TestCase(png0Rgba32, png0Rgba32)]
public void ShouldVerifyThatImageSharpImagesAreEqualBrga(string pathActual, string pathExpected)
{
var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);

using var actual = Image.Load(absolutePathActual);
using var expected = Image.Load<Bgra32>(absolutePathExpected);

Assert.That(ImageSharpCompare.ImagesAreEqual(actual, expected), Is.True);
AssertDisposeBehavior.AssertThatImageIsDisposed(actual);
AssertDisposeBehavior.AssertThatImageIsDisposed(expected);
}

[Test]
[TestCase(jpg0Rgb24, jpg0Rgb24)]
[TestCase(png0Rgba32, png0Rgba32)]
public void ShouldVerifyThatImageSharpImagesAreEqualBgra5551(string pathActual, string pathExpected)
{
var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);

using var actual = Image.Load<Bgra5551>(absolutePathActual);
using var expected = Image.Load<Bgra5551>(absolutePathExpected);

Assert.That(ImageSharpCompare.ImagesAreEqual(actual, expected), Is.True);
AssertDisposeBehavior.AssertThatImageIsDisposed(actual);
AssertDisposeBehavior.AssertThatImageIsDisposed(expected);
}

[Test]
Expand Down Expand Up @@ -254,10 +285,43 @@ public void ShouldCalcDiffMaskImageSharpAndUseOutcome(string pathPic1, string pa
Assert.That(maskedDiff.MeanError, Is.EqualTo(expectedMeanError), "MeanError");
Assert.That(maskedDiff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount");
Assert.That(maskedDiff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage");
AssertDisposeBehavior.
AssertThatImageIsDisposed(absolutePic1);
AssertDisposeBehavior.AssertThatImageIsDisposed(absolutePic2);
AssertDisposeBehavior.AssertThatImageIsDisposed(differenceMaskPic);
}

AssertDisposeBehavior(absolutePic1);
AssertDisposeBehavior(absolutePic2);
AssertDisposeBehavior(differenceMaskPic);
[TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize)]
[TestCase(jpg0Rgb24, jpg1Rgb24, 0, 0, 0, 0, ResizeOption.DontResize)]
[TestCase(jpg0Rgb24, jpg1Rgb24, 0, 0, 0, 0, ResizeOption.Resize)]
[TestCase(pngBlack2x2px, pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize)]
public void ShouldCalcDiffMaskImageHalfSingleHalfVector2AndUseOutcome(string pathPic1, string pathPic2, int expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption)
{
var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
var differenceMaskPicPath = Path.GetTempFileName() + "differenceMask.png";

using var absolutePic1 = Image.Load<HalfVector2>(absolutePathPic1);
using var absolutePic2 = Image.Load(absolutePathPic2);

using (var fileStreamDifferenceMask = File.Create(differenceMaskPicPath))
using (var maskImage = ImageSharpCompare.CalcDiffMaskImage(absolutePic1, absolutePic2, resizeOption))
{
ImageExtensions.SaveAsPng(maskImage, fileStreamDifferenceMask);
}

using var differenceMaskPic = Image.Load(differenceMaskPicPath);
var maskedDiff = ImageSharpCompare.CalcDiff(absolutePic1, absolutePic2, differenceMaskPic, resizeOption);
File.Delete(differenceMaskPicPath);

Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError");
Assert.That(maskedDiff.MeanError, Is.EqualTo(expectedMeanError), "MeanError");
Assert.That(maskedDiff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount");
Assert.That(maskedDiff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage");
AssertDisposeBehavior.
AssertThatImageIsDisposed(absolutePic1);
AssertDisposeBehavior.AssertThatImageIsDisposed(absolutePic2);
AssertDisposeBehavior.AssertThatImageIsDisposed(differenceMaskPic);
}

[TestCase(pngWhite2x2px, pngBlack2x2px, pngTransparent2x2px, 765, 12240, 16, 100d, ResizeOption.Resize, 0)]
Expand All @@ -281,10 +345,10 @@ public void ShouldUseDiffMask(string pathPic1, string pathPic2, string pathPic3,
Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError");
Assert.That(maskedDiff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount");
Assert.That(maskedDiff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage");

AssertDisposeBehavior(pic1);
AssertDisposeBehavior(pic2);
AssertDisposeBehavior(maskPic);
AssertDisposeBehavior.
AssertThatImageIsDisposed(pic1);
AssertDisposeBehavior.AssertThatImageIsDisposed(pic2);
AssertDisposeBehavior.AssertThatImageIsDisposed(maskPic);
}

[TestCase(pngBlack2x2px, pngBlack2x2px, pngBlack4x4px)]
Expand All @@ -302,28 +366,13 @@ public void ShouldThrowUsingInvalidImageDimensionsDiffMask(string pathPic1, stri
var exception = Assert.Throws<ImageSharpCompareException>(() => ImageSharpCompare.CalcDiff(pic1, pic2, maskPic, ResizeOption.DontResize));

Assert.That(exception?.Message, Is.EqualTo("Size of images differ."));

AssertDisposeBehavior(pic1);
AssertDisposeBehavior(pic2);
AssertDisposeBehavior(maskPic);
AssertDisposeBehavior.
AssertThatImageIsDisposed(pic1);
AssertDisposeBehavior.AssertThatImageIsDisposed(pic2);
AssertDisposeBehavior.AssertThatImageIsDisposed(maskPic);
}

private static void AssertDisposeBehavior(Image image)
{
const string imageSharpPrivateFieldNameIsDisposed = "isDisposed";
var isDisposed = (bool?)GetInstanceField(image, imageSharpPrivateFieldNameIsDisposed);
Assert.That(isDisposed, Is.False);
image.Dispose();
isDisposed = (bool?)GetInstanceField(image, imageSharpPrivateFieldNameIsDisposed);
Assert.That(isDisposed, Is.True);
}

private static object? GetInstanceField<T>(T instance, string fieldName)
{
var bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
var field = typeof(T).GetField(fieldName, bindFlags);
return field == null ? throw new ArgumentNullException(fieldName) : field.GetValue(instance);
}

[TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0)]
public void DiffMaskSteams(string pathPic1, string pathPic2, int expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage)
Expand Down
8 changes: 4 additions & 4 deletions ImageSharpCompareTestNunit/ImageSharpCompareTestNunit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,20 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PackageReference Include="coverlet.collector" Version="6.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="nunit" Version="4.0.1" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.18.0.83559">
<PackageReference Include="nunit" Version="4.1.0" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="9.24.0.89429">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
</ItemGroup>

<ItemGroup>
Expand Down
46 changes: 46 additions & 0 deletions ImageSharpCompareTestNunit/ImageSharpPixelTypeConverterTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Codeuctivity.ImageSharpCompare;
using NUnit.Framework;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using System;
using System.IO;

namespace ImageSharpCompareTestNunit
{
public class ImageSharpPixelTypeConverterTests
{
private const string pngBlack2x2px = "../../../TestData/Black.png";

[Test]
public void ShouldConvertToRgb24()
{
var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pngBlack2x2px);

var image = Image.Load<Rgb24>(absolutePathActual);

var rgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(image, out var isClonedInstance);

Assert.That(isClonedInstance, Is.False);
Assert.That(rgb24.PixelType.BitsPerPixel, Is.EqualTo(24));

image.Dispose();
AssertDisposeBehavior.AssertThatImageIsDisposed(rgb24, true);
}

[Test]
public void ShouldConvertAnyPixelTypeToRgb24()
{
var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pngBlack2x2px);

var image = Image.Load<Rgba64>(absolutePathActual);

var rgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(image, out var isClonedInstance);

Assert.That(isClonedInstance, Is.True);
Assert.That(rgb24.PixelType.BitsPerPixel, Is.EqualTo(24));

image.Dispose();
AssertDisposeBehavior.AssertThatImageIsDisposed(rgb24);
}
}
}