diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..a1d7649 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: [Codeuctivity] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: stesee +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index de500a2..686f96f 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -20,6 +20,7 @@ jobs: with: dotnet-version: | 8.0.x + 9.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -38,6 +39,7 @@ jobs: with: dotnet-version: | 8.0.x + 9.0.x - name: Restore dependencies run: dotnet restore - name: Build @@ -67,6 +69,7 @@ jobs: with: dotnet-version: | 8.0.x + 9.0.x - name: Check formatting run: dotnet format --verify-no-changes - name: Restore dependencies diff --git a/ImageSharpCompare/ImageSharpCompare.cs b/ImageSharpCompare/ImageSharpCompare.cs index 67b3723..80f8b8a 100644 --- a/ImageSharpCompare/ImageSharpCompare.cs +++ b/ImageSharpCompare/ImageSharpCompare.cs @@ -98,12 +98,13 @@ public static bool ImagesHaveEqualSize(Image actualImage, Image expectedImage) /// /// /// + /// /// True if every pixel of actual is equal to expected - public static bool ImagesAreEqual(string pathImageActual, string pathImageExpected, ResizeOption resizeOption = ResizeOption.DontResize) + public static bool ImagesAreEqual(string pathImageActual, string pathImageExpected, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) { using var actualImage = Image.Load(pathImageActual); using var expectedImage = Image.Load(pathImageExpected); - return ImagesAreEqual(actualImage, expectedImage, resizeOption); + return ImagesAreEqual(actualImage, expectedImage, resizeOption, pixelColorShiftTolerance); } /// @@ -126,8 +127,9 @@ public static bool ImagesAreEqual(Stream actual, Stream expected, ResizeOption r /// /// /// + /// /// True if every pixel of actual is equal to expected - public static bool ImagesAreEqual(Image actual, Image expected, ResizeOption resizeOption = ResizeOption.DontResize) + public static bool ImagesAreEqual(Image actual, Image expected, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) { ArgumentNullException.ThrowIfNull(actual); @@ -142,7 +144,7 @@ public static bool ImagesAreEqual(Image actual, Image expected, ResizeOption res actualPixelAccessibleImage = ImageSharpPixelTypeConverter.ToRgb24Image(actual, out ownsActual); expectedPixelAccusableImage = ImageSharpPixelTypeConverter.ToRgb24Image(expected, out ownsExpected); - return ImagesAreEqual(actualPixelAccessibleImage, expectedPixelAccusableImage, resizeOption); + return ImagesAreEqual(actualPixelAccessibleImage, expectedPixelAccusableImage, resizeOption, pixelColorShiftTolerance); } finally { @@ -163,8 +165,9 @@ public static bool ImagesAreEqual(Image actual, Image expected, ResizeOption res /// /// /// + /// /// True if every pixel of actual is equal to expected - public static bool ImagesAreEqual(Image actual, Image expected, ResizeOption resizeOption = ResizeOption.DontResize) + public static bool ImagesAreEqual(Image actual, Image expected, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) { ArgumentNullException.ThrowIfNull(actual); @@ -181,10 +184,21 @@ public static bool ImagesAreEqual(Image actual, Image expected, Re { for (var y = 0; y < actual.Height; y++) { - if (!actual[x, y].Equals(expected[x, y])) + if (pixelColorShiftTolerance == 0 && !actual[x, y].Equals(expected[x, y])) { return false; } + else if (pixelColorShiftTolerance > 0) + { + var actualPixel = actual[x, y]; + var expectedPixel = expected[x, y]; + if (Math.Abs(actualPixel.R - expectedPixel.R) > pixelColorShiftTolerance || + Math.Abs(actualPixel.G - expectedPixel.G) > pixelColorShiftTolerance || + Math.Abs(actualPixel.B - expectedPixel.B) > pixelColorShiftTolerance) + { + return false; + } + } } } @@ -194,7 +208,7 @@ public static bool ImagesAreEqual(Image actual, Image expected, Re var grown = GrowToSameDimension(actual, expected); try { - return ImagesAreEqual(grown.Item1, grown.Item2, ResizeOption.DontResize); + return ImagesAreEqual(grown.Item1, grown.Item2, ResizeOption.DontResize, pixelColorShiftTolerance); } finally { @@ -350,7 +364,7 @@ public static ICompareResult CalcDiff(Image actual, Image expected var g = Math.Abs(expectedPixel.G - actualPixel.G); var b = Math.Abs(expectedPixel.B - actualPixel.B); var sum = r + g + b; - absoluteError += (sum > pixelColorShiftTolerance ? sum : 0); + absoluteError += sum > pixelColorShiftTolerance ? sum : 0; pixelErrorCount += (sum > pixelColorShiftTolerance) ? 1 : 0; } } @@ -477,7 +491,7 @@ public static ICompareResult CalcDiff(Image actual, Image expected error += b; } - absoluteError += (error > pixelColorShiftTolerance ? error : 0); + absoluteError += error > pixelColorShiftTolerance ? error : 0; pixelErrorCount += error > pixelColorShiftTolerance ? 1 : 0; } } @@ -663,14 +677,40 @@ public static Image CalcDiffMaskImage(Image actual, Image expected var actualPixel = actual[x, y]; var expectedPixel = expected[x, y]; - var pixel = new Rgb24 + if (pixelColorShiftTolerance == 0) { - R = (byte)Math.Abs(actualPixel.R - expectedPixel.R), - G = (byte)Math.Abs(actualPixel.G - expectedPixel.G), - B = (byte)Math.Abs(actualPixel.B - expectedPixel.B) - }; - - maskImage[x, y] = pixel; + var pixel = new Rgb24 + { + R = (byte)Math.Abs(actualPixel.R - expectedPixel.R), + G = (byte)Math.Abs(actualPixel.G - expectedPixel.G), + B = (byte)Math.Abs(actualPixel.B - expectedPixel.B) + }; + + maskImage[x, y] = pixel; + } + else + { + var r = Math.Abs(actualPixel.R - expectedPixel.R); + var g = Math.Abs(actualPixel.G - expectedPixel.G); + var b = Math.Abs(actualPixel.B - expectedPixel.B); + + var error = r + g + b; + if (error <= pixelColorShiftTolerance) + { + r = 0; + g = 0; + b = 0; + } + + var pixel = new Rgb24 + { + R = (byte)r, + G = (byte)g, + B = (byte)b + }; + + maskImage[x, y] = pixel; + } } } return maskImage; diff --git a/ImageSharpCompare/ImageSharpCompare.csproj b/ImageSharpCompare/ImageSharpCompare.csproj index 1432d6d..1d9d497 100644 --- a/ImageSharpCompare/ImageSharpCompare.csproj +++ b/ImageSharpCompare/ImageSharpCompare.csproj @@ -1,6 +1,6 @@ - net8.0 + net8.0;net9.0 true true https://github.com/Codeuctivity/ImageSharp.Compare @@ -45,7 +45,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/ImageSharpCompare/ImageSharpCompare.sln b/ImageSharpCompare/ImageSharpCompare.sln new file mode 100644 index 0000000..001bc91 --- /dev/null +++ b/ImageSharpCompare/ImageSharpCompare.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharpCompare", "ImageSharpCompare.csproj", "{6D218566-E4FA-E901-D111-08AEB4065B5C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6D218566-E4FA-E901-D111-08AEB4065B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D218566-E4FA-E901-D111-08AEB4065B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D218566-E4FA-E901-D111-08AEB4065B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D218566-E4FA-E901-D111-08AEB4065B5C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {239FA1C2-CD5C-4D95-BE3C-97B7B1BF4F3C} + EndGlobalSection +EndGlobal diff --git a/ImageSharpCompareTestNunit/ImageSharpCompareTest.cs b/ImageSharpCompareTestNunit/ImageSharpCompareTest.cs index 26b59a5..60eea72 100644 --- a/ImageSharpCompareTestNunit/ImageSharpCompareTest.cs +++ b/ImageSharpCompareTestNunit/ImageSharpCompareTest.cs @@ -93,6 +93,18 @@ public void ShouldVerifyThatImagesWithDifferentSizeAreEqual(string pathActual, s Assert.That(ImageSharpCompare.ImagesAreEqual(absolutePathActual, absolutePathExpected, resizeOption), Is.EqualTo(expectedResult)); } + [Test] + [TestCase(colorShift1, colorShift2, ResizeOption.DontResize, 0, false)] + [TestCase(colorShift1, colorShift2, ResizeOption.Resize, 0, false)] + [TestCase(colorShift1, colorShift2, ResizeOption.DontResize, 15, true)] + [TestCase(colorShift1, colorShift2, ResizeOption.Resize, 15, true)] + public void ShouldVerifyThatImagesWithColorShift(string pathActual, string pathExpected, ResizeOption resizeOption, int expectedColorShift, bool expectedResult) + { + var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual); + var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected); + + Assert.That(ImageSharpCompare.ImagesAreEqual(absolutePathActual, absolutePathExpected, resizeOption, expectedColorShift), Is.EqualTo(expectedResult)); + } [Test] [TestCase(jpg0Rgb24, jpg0Rgb24)] @@ -157,7 +169,6 @@ public void ShouldVerifyThatImageSharpImagesAreEqualBgra5551(string pathActual, } [Test] - [TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, null, 0)] [TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, ResizeOption.DontResize, 0)] [TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, ResizeOption.Resize, 0)] [TestCase(jpg1Rgb24, png1Rgba32, 382669, 2.3673566603152607d, 140893, 87.162530004206772d, ResizeOption.DontResize, 0)] @@ -204,7 +215,6 @@ public void ShouldVerifyThatCalcDiffThrowsOnDifferentImageSizes(string pathPic1, } [Test] - [TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, null)] [TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, ResizeOption.DontResize)] [TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, ResizeOption.Resize)] [TestCase(jpg1Rgb24, png1Rgba32, 382669, 2.3673566603152607d, 140893, 87.162530004206772d, ResizeOption.DontResize)] @@ -230,26 +240,32 @@ public void ShouldVerifyThatImageStreamsAreSemiEqual(string pathPic1, string pat Assert.That(diff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage"); } - [TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, null)] - [TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize)] - [TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.Resize)] - [TestCase(pngWhite2x2px, pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize)] - [TestCase(pngBlack4x4px, pngWhite2x2px, 0, 0, 0, 0, ResizeOption.Resize)] - [TestCase(renderedForm1, renderedForm2, 0, 0, 0, 0, ResizeOption.Resize)] - [TestCase(renderedForm2, renderedForm1, 0, 0, 0, 0, ResizeOption.Resize)] - public void CalcDiffMaskImage(string pathPic1, string pathPic2, double expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption) + [TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize, 0, false)] + [TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.Resize, 0, false)] + [TestCase(pngWhite2x2px, pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize, 0, false)] + [TestCase(pngBlack4x4px, pngWhite2x2px, 0, 0, 0, 0, ResizeOption.Resize, 0, false)] + [TestCase(renderedForm1, renderedForm2, 0, 0, 0, 0, ResizeOption.Resize, 0, false)] + [TestCase(renderedForm2, renderedForm1, 0, 0, 0, 0, ResizeOption.Resize, 0, false)] + [TestCase(colorShift1, colorShift1, 0, 0, 0, 0, ResizeOption.DontResize, 15, true)] + [TestCase(colorShift1, colorShift1, 0, 0, 0, 0, ResizeOption.Resize, 15, true)] + [TestCase(colorShift1, colorShift2, 0, 0, 0, 0, ResizeOption.Resize, 15, true)] + [TestCase(colorShift1, colorShift2, 0, 0, 0, 0, ResizeOption.DontResize, 15, true)] + [TestCase(colorShift1, colorShift2, 0, 0, 0, 0, ResizeOption.Resize, 14, false)] + [TestCase(colorShift1, colorShift2, 0, 0, 0, 0, ResizeOption.DontResize, 14, false)] + public void CalcDiffMaskImage(string pathPic1, string pathPic2, double expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption, int expectedColorShift, bool expectMaskToBeBlack) { var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1); var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2); var differenceMask = Path.GetTempFileName() + "differenceMask.png"; using (var fileStreamDifferenceMask = File.Create(differenceMask)) - using (var maskImage = ImageSharpCompare.CalcDiffMaskImage(absolutePathPic1, absolutePathPic2, resizeOption)) + using (var maskImage = ImageSharpCompare.CalcDiffMaskImage(absolutePathPic1, absolutePathPic2, resizeOption, expectedColorShift)) { ImageExtensions.SaveAsPng(maskImage, fileStreamDifferenceMask); + Assert.That(IsImageEntirelyBlack(maskImage), Is.EqualTo(expectMaskToBeBlack)); } - var maskedDiff = ImageSharpCompare.CalcDiff(absolutePathPic1, absolutePathPic2, differenceMask, resizeOption); + var maskedDiff = ImageSharpCompare.CalcDiff(absolutePathPic1, absolutePathPic2, differenceMask, resizeOption, expectedColorShift); File.Delete(differenceMask); Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError"); diff --git a/ImageSharpCompareTestNunit/ImageSharpCompareTestNunit.csproj b/ImageSharpCompareTestNunit/ImageSharpCompareTestNunit.csproj index c82f334..e463c48 100644 --- a/ImageSharpCompareTestNunit/ImageSharpCompareTestNunit.csproj +++ b/ImageSharpCompareTestNunit/ImageSharpCompareTestNunit.csproj @@ -1,27 +1,31 @@  - net8.0 + net8.0;net9.0 false enable true - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + all runtime; build; native; contentfiles; analyzers; buildtransitive - +