diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..06b72da
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,34 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
+{
+ "name": "C# (.NET)",
+ // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
+ "image": "mcr.microsoft.com/devcontainers/dotnet:2-10.0-noble",
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "ms-dotnettools.csdevkit"
+ ]
+ }
+ }
+
+ // Features to add to the dev container. More info: https://containers.dev/features.
+ // "features": {},
+
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
+ // "forwardPorts": [5000, 5001],
+ // "portsAttributes": {
+ // "5001": {
+ // "protocol": "https"
+ // }
+ // }
+
+ // Use 'postCreateCommand' to run commands after the container is created.
+ // "postCreateCommand": "dotnet restore",
+
+ // Configure tool-specific properties.
+ // "customizations": {},
+
+ // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
+ // "remoteUser": "root"
+}
diff --git a/.editorconfig b/.editorconfig
index 71c3975..3ca6b4f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,13 +1,22 @@
-[*.cs]
+[*]
+spelling_exclusion_path = .\exclusion.dic
+
+[*.cs]
# CA1303: Do not pass literals as localized parameters
dotnet_diagnostic.CA1303.severity = none
+# CA2000: Dispose objects before losing scope
+dotnet_diagnostic.CA2000.severity = suggestion
+
# var preferences
csharp_style_var_elsewhere = true:warning
csharp_style_var_for_built_in_types = true:warning
csharp_style_var_when_type_is_apparent = true:warning
+# CA1822: Mark members as static
+dotnet_diagnostic.CA1822.severity = suggestion
+
[*.{cs,vb}]
tab_width=4
indent_size=4
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..36ace77
--- /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: # Replace with a single Buy Me a Coffee username
+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/cla.yml b/.github/workflows/cla.yml
index c38e82b..3152501 100644
--- a/.github/workflows/cla.yml
+++ b/.github/workflows/cla.yml
@@ -11,17 +11,17 @@ jobs:
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
- uses: cla-assistant/github-action@v2.2.1
+ uses: cla-assistant/github-action@v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret
PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }}
with:
path-to-signatures: 'signatures/version1/cla.json'
- path-to-document: 'https://github.com/Codeuctivity/ImageSharp.Compare/blob/main/cla.md' # e.g. a CLA or a DCO document
+ path-to-document: 'https://github.com/Codeuctivity/SkiaSharp.Compare/blob/main/cla.md' # e.g. a CLA or a DCO document
# branch should not be protected
branch: 'cla'
- allowlist: dependabot[bot],stesee
+ allowlist: dependabot[bot],stesee,github-actions[bot],github-copilot[bot],copilot[bot],copilot-swe-agent[bot],Copilot
#below are the optional inputs - If the optional inputs are not given, then default values will be taken
#remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository)
diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml
index 95422b1..ca8b1c2 100644
--- a/.github/workflows/dotnet.yml
+++ b/.github/workflows/dotnet.yml
@@ -1,11 +1,15 @@
name: .NET build and test
env:
- CURRENT_VERSION: 0.0.${{ github.run_number }}
+ CURRENT_VERSION: 4.0.${{ github.run_number }}
LAST_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
on:
push:
- pull_request:
+ paths-ignore:
+ - '**.md'
+
+permissions:
+ contents: write
jobs:
build:
@@ -14,79 +18,104 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v7
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v6
with:
dotnet-version: |
- 6.0.x
+ 10.0.x
+ - name: Check formatting
+ run: dotnet format --verify-no-changes
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
+
+ - name: Publish SkiaSharpCompare.Cli (Linux)
+ if: runner.os == 'Linux'
+ run: |
+ dotnet publish SkiaSharpCompare.Cli/SkiaSharpCompare.Cli.csproj -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -o ./artifacts/cli/linux-x64
+
+ - name: Publish SkiaSharpCompare.Cli (macOS)
+ if: runner.os == 'macOS'
+ run: |
+ dotnet publish SkiaSharpCompare.Cli/SkiaSharpCompare.Cli.csproj -c Release -r osx-x64 --self-contained true -p:PublishSingleFile=true -o ./artifacts/cli/osx-x64
+
+ - name: Publish SkiaSharpCompare.Cli (Windows)
+ if: runner.os == 'Windows'
+ shell: powershell
+ run: |
+ dotnet publish SkiaSharpCompare.Cli/SkiaSharpCompare.Cli.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o ./artifacts/cli/win-x64
+ dotnet publish SkiaSharpCompare.Cli/SkiaSharpCompare.Cli.csproj -c Release -r win-arm64 --self-contained true -p:PublishSingleFile=true -o ./artifacts/cli/win-arm64
+
+ - name: Upload CLI artifacts
+ uses: actions/upload-artifact@v7
+ with:
+ name: cli-artifacts-${{ matrix.os }}
+ path: ./artifacts/cli
+ retention-days: 7
+
- name: Test
run: dotnet test --no-build --verbosity normal --configuration Release
deployRelease:
- if: github.ref == 'refs/heads/release'
+ if: startsWith(github.ref, 'refs/heads/release')
runs-on: ubuntu-latest
- needs: build
+ needs: build
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v7
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v6
with:
dotnet-version: |
- 6.0.x
+ 10.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
+ - name: Download CLI artifacts
+ uses: actions/download-artifact@v8
+ with:
+ path: ./artifacts_download
- name: NugetPush
env:
NUGET_TOKEN_EXISTS: ${{ secrets.NUGET_TOKEN }}
- if: env.NUGET_TOKEN_EXISTS != ''
+ if: env.NUGET_TOKEN_EXISTS != ''
run: |
- dotnet nuget push ./SkiaSharpCompare/bin/Release/*.nupkg --skip-duplicate --api-key ${{secrets.NUGET_TOKEN}} --source https://api.nuget.org/v3/index.json
- - uses: "marvinpinto/action-automatic-releases@latest"
- with:
- repo_token: "${{ secrets.GITHUB_TOKEN }}"
- automatic_release_tag: ${{ env.CURRENT_VERSION }}
- prerelease: false
- title: "Release Build"
- files: |
- ./SkiaSharpCompare/bin/Release/*.nupkg
- ./SkiaSharpCompare/bin/Release/*.snupkg
+ dotnet nuget push ./SkiaSharpCompare/bin/Release/*.nupkg --skip-duplicate --api-key ${{secrets.NUGET_TOKEN}} --source https://api.nuget.org/v3/index.json
+ - name: Github release
+ shell: bash
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ if: env.GITHUB_TOKEN != ''
+ run: |
+ # Attach all downloaded CLI artifacts regardless of OS
+ zip -r cli-artifacts.zip ./artifacts_download
+ gh release create ${{env.CURRENT_VERSION}} ./SkiaSharpCompare/bin/Release/*.*nupkg cli-artifacts.zip --generate-notes
deployTest:
- if: github.ref != 'refs/heads/release'
+ if: ${{ github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
- needs: build
+ needs: build
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v7
- name: Setup .NET
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v6
with:
dotnet-version: |
- 6.0.x
+ 10.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
+ - name: Download CLI artifacts
+ uses: actions/download-artifact@v8
+ with:
+ path: ./artifacts_download
- name: NugetPush
env:
NUGET_TOKEN_EXISTS: ${{ secrets.NUGET_TEST_TOKEN }}
- if: env.NUGET_TOKEN_EXISTS != ''
+ if: env.NUGET_TOKEN_EXISTS != ''
run: |
ls ./SkiaSharpCompare/bin/Release
- dotnet nuget push ./SkiaSharpCompare/bin/Release/*.nupkg --skip-duplicate --api-key ${{secrets.NUGET_TEST_TOKEN}} --source https://apiint.nugettest.org/v3/index.json
- - uses: "marvinpinto/action-automatic-releases@latest"
- if: github.ref == 'refs/heads/main'
- with:
- repo_token: "${{ secrets.GITHUB_TOKEN }}"
- automatic_release_tag: "latest-prerelease"
- prerelease: true
- title: "Prerelease Build"
- files: |
- ./SkiaSharpCompare/bin/Release/*.nupkg
- ./SkiaSharpCompare/bin/Release/*.snupkg
+ dotnet nuget push ./SkiaSharpCompare/bin/Release/*.nupkg --skip-duplicate --api-key ${{secrets.NUGET_TEST_TOKEN}} --source https://apiint.nugettest.org/v3/index.json
\ No newline at end of file
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 655d659..9ca7111 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -18,7 +18,7 @@ jobs:
pull-requests: write
steps:
- - uses: actions/stale@v6
+ - uses: actions/stale@v11
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Stale issue message'
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 3704b2e..f803ec5 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -2,7 +2,6 @@
"recommendations": [
"mhutchie.git-graph",
"streetsidesoftware.code-spell-checker",
- "timonwong.shellcheck",
"redhat.vscode-xml",
"redhat.vscode-yaml"
]
diff --git a/.vscode/launch.json b/.vscode/launch.json
index da20669..0b302ee 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -10,9 +10,9 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
- "program": "${workspaceFolder}/ImageSharpCompareTestNunit/bin/Debug/net6.0/ImageSharpCompareTestNunit.dll",
+ "program": "${workspaceFolder}/SkiaSharpCompareTestNunit/bin/Debug/net6.0/SkiaSharpCompareTestNunit.dll",
"args": [],
- "cwd": "${workspaceFolder}/ImageSharpCompareTestNunit",
+ "cwd": "${workspaceFolder}/SkiaSharpCompareTestNunit",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
diff --git a/.vscode/settings.json b/.vscode/settings.json
index ae6673f..201e0e3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,11 +1,14 @@
{
- "cSpell.words": [
- "Codacy",
- "Codeuctivity",
- "Nuget",
- "Nunit",
- "Rgba",
- "nupkg",
- "snupkg"
- ]
-}
\ No newline at end of file
+ "cSpell.words": [
+ "Codacy",
+ "Codeuctivity",
+ "Diffmask",
+ "Dont",
+ "Nuget",
+ "Nunit",
+ "nupkg",
+ "Rgba",
+ "Skia",
+ "snupkg"
+ ]
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index d0235d3..c22253a 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -7,7 +7,7 @@
"type": "process",
"args": [
"build",
- "${workspaceFolder}/ImageSharpCompareTestNunit/ImageSharpCompareTestNunit.csproj",
+ "${workspaceFolder}/SkiaSharpCompareTestNunit/SkiaSharpCompareTestNunit.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
@@ -19,7 +19,7 @@
"type": "process",
"args": [
"publish",
- "${workspaceFolder}/ImageSharpCompareTestNunit/ImageSharpCompareTestNunit.csproj",
+ "${workspaceFolder}/SkiaSharpCompareTestNunit/SkiaSharpCompareTestNunit.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
@@ -32,7 +32,7 @@
"args": [
"watch",
"run",
- "${workspaceFolder}/ImageSharpCompareTestNunit/ImageSharpCompareTestNunit.csproj",
+ "${workspaceFolder}/SkiaSharpCompareTestNunit/SkiaSharpCompareTestNunit.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
@@ -43,6 +43,18 @@
"type": "shell",
"command": "'./.vscode/updateNuget.sh'",
"problemMatcher": []
+ },
+ {
+ "label": "update nuget packages (windows)",
+ "type": "shell",
+ "command": "powershell",
+ "args": [
+ "-ExecutionPolicy",
+ "Bypass",
+ "-File",
+ "./.vscode/updateNuget.ps1"
+ ],
+ "problemMatcher": []
}
]
}
\ No newline at end of file
diff --git a/.vscode/updateNuget.ps1 b/.vscode/updateNuget.ps1
new file mode 100644
index 0000000..8977003
--- /dev/null
+++ b/.vscode/updateNuget.ps1
@@ -0,0 +1,23 @@
+# PowerShell script to update NuGet packages in all project files
+# This script finds all .csproj and .fsproj files and updates stable (non-prerelease) packages
+
+$regex = 'PackageReference Include="([^"]*)" Version="([^"]*)"'
+
+Get-ChildItem -Path . -Recurse -Include "*.csproj", "*.fsproj" | ForEach-Object {
+ $projFile = $_.FullName
+ Write-Host "Processing project: $projFile"
+ $content = Get-Content $projFile
+ foreach ($line in $content) {
+ if ($line -match $regex) {
+ $packageName = $matches[1]
+ $version = $matches[2]
+ Write-Host "Found package: $packageName, version: $version"
+
+ # Only update stable versions (not prerelease versions containing -)
+ if ($version -notmatch '-') {
+ Write-Host "Updating package: $packageName"
+ dotnet add "$projFile" package "$packageName"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index b5dccc9..408a378 100644
--- a/README.md
+++ b/README.md
@@ -8,15 +8,26 @@ Inspired by the image compare feature "Visual verification API" of [TestApi](htt
SkiaSharpCompare focus on os agnostic support and therefore depends on [SkiaSharp](https://github.com/mono/SkiaSharp).
-**NOTE**: For now the **A**lpha-channel is ignored.
-
## Example simple show cases
+### Compares each RGB value of each pixel to determine the equality
+
```csharp
-bool imagesAreEqual = SkiaSharpCompare.ImagesAreEqual("actual.png", "expected.png");
+bool isEqual = Compare.ImagesAreEqual("actual.png", "expected.png");
+```
-// calcs MeanError, AbsoluteError, PixelErrorCount and PixelErrorPercentage
-ICompareResult calcDiff = SkiaSharpCompare.CalcDiff("actual.png", "expected.png");
+### [Calculates diff](https://dotnetfiddle.net/tTnq2j)
+
+```csharp
+var calcDiff = Compare.CalcDiff("2x2PixelBlack.png", "2x2PixelWhite.png");
+Console.WriteLine($"PixelErrorCount: {calcDiff.PixelErrorCount}");
+Console.WriteLine($"PixelErrorPercentage: {calcDiff.PixelErrorPercentage}");
+Console.WriteLine($"AbsoluteError: {calcDiff.AbsoluteError}");
+Console.WriteLine($"MeanError: {calcDiff.MeanError}");
+// PixelErrorCount: 4
+// PixelErrorPercentage: 100
+// AbsoluteError: 3060
+// MeanError: 765
```
## Example show case allowing some tolerated diff
@@ -33,7 +44,7 @@ Imagine two images you want to compare, and want to accept the found difference
### Tolerance mask image
-using "compare.CalcDiff" you can calc a diff mask from actual and reference image
+Using **CalcDiffMaskImage** you can calc a diff mask from actual and reference image
Example - Create difference image
@@ -52,3 +63,29 @@ Example - Compare two images using the created difference image. Add white pixel
var maskedDiff = SkiaSharpCompare.CalcDiff(pathPic1, pathPic2, "differenceMask.png");
Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(0));
```
+
+### [Configure transparency, enable metadata compare, ...](https://dotnetfiddle.net/lygaRU)
+
+```csharp
+var comparer = new ImageCompare(ResizeOption.Resize, TransparencyOptions.CompareAlphaChannel, pixelColorShiftTolerance: 5, compareMetadata: true);
+var calcDiff = comparer.CalcDiff("pngPartialTransparent4x4Pixel.png", "2x2PixelWhite.png");
+// Displaying the differences
+Console.WriteLine($"PixelErrorCount: {calcDiff.PixelErrorCount}");
+Console.WriteLine($"PixelErrorPercentage: {calcDiff.PixelErrorPercentage}");
+Console.WriteLine($"AbsoluteError: {calcDiff.AbsoluteError}");
+Console.WriteLine($"MeanError: {calcDiff.MeanError}");
+foreach (var metadataDifference in calcDiff.MetadataDifferences)
+ Console.WriteLine($"Metadata Difference: {metadataDifference}");
+
+// PixelErrorCount: 16
+// PixelErrorPercentage: 100
+// AbsoluteError: 14272
+// MeanError: 892
+// Metadata Difference: [File:File Name, (pngPartialTransparent4x4Pixel.png, 2x2PixelWhite.png)]
+// Metadata Difference: [File:File Size, (688 bytes, 556 bytes)]
+// Metadata Difference: [PNG-IHDR:Color Type, (True Color with Alpha, True Color)]
+// Metadata Difference: [PNG-IHDR:Image Height, (4, 2)]
+// Metadata Difference: [PNG-IHDR:Image Width, (4, 2)]
+// Metadata Difference: [PNG-tEXt:Textual Data, (Software: Paint.NET 5.1.2, Comment: Created with GIMP)]
+// ...
+```
diff --git a/SkiaSharpCompare.Cli.Tests/SkiaSharpCompare.Cli.Tests.csproj b/SkiaSharpCompare.Cli.Tests/SkiaSharpCompare.Cli.Tests.csproj
new file mode 100644
index 0000000..199fde0
--- /dev/null
+++ b/SkiaSharpCompare.Cli.Tests/SkiaSharpCompare.Cli.Tests.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net10.0
+ false
+ 12
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SkiaSharpCompare.Cli.Tests/SkiaSharpCompareCliIntegrationTests.cs b/SkiaSharpCompare.Cli.Tests/SkiaSharpCompareCliIntegrationTests.cs
new file mode 100644
index 0000000..c78cf6c
--- /dev/null
+++ b/SkiaSharpCompare.Cli.Tests/SkiaSharpCompareCliIntegrationTests.cs
@@ -0,0 +1,64 @@
+using NUnit.Framework;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace SkiaSharpCompare.Cli.Tests
+{
+ public class SkiaSharpCompareCliIntegrationTests
+ {
+ private const int ProcessTimeoutMs = 30_000;
+
+ [Test]
+ public async Task SkiaSharpCompareCli_ShouldExitSuccessfully()
+ {
+ // Static precompiled CLI project directory (relative to test assembly output).
+ var cliProjectDir = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "SkiaSharpCompare.Cli"));
+ Assert.That(Directory.Exists(cliProjectDir), Is.True, $"Could not locate CLI project directory at '{cliProjectDir}'. Ensure the solution layout is unchanged and the path is valid.");
+
+ // Find a JPG file in the repository to pass to the CLI.
+ var jpgFile = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "SkiaSharpCompareTestNunit", "TestData", "imageWithGpsMetadata.jpg"));
+ Assert.That(jpgFile, Is.Not.Null, "Could not locate any .jpg file in the repository to use for the integration test.");
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "dotnet",
+ Arguments = $"run -- \"{jpgFile}\" --meta",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = false,
+ WorkingDirectory = cliProjectDir
+ };
+
+ using var process = new Process { StartInfo = startInfo };
+
+ process.Start();
+
+ var stdOutTask = process.StandardOutput.ReadToEndAsync();
+ var stdErrTask = process.StandardError.ReadToEndAsync();
+
+ var exited = await Task.Run(() => process.WaitForExit(ProcessTimeoutMs));
+ if (!exited)
+ {
+ try
+ {
+ process.Kill(entireProcessTree: true);
+ }
+ catch
+ {
+ // Best effort
+ }
+
+ Assert.Fail($"Process did not exit within {ProcessTimeoutMs} ms. StdOut:{Environment.NewLine}{await stdOutTask}{Environment.NewLine}StdErr:{Environment.NewLine}{await stdErrTask}");
+ }
+
+ var stdout = await stdOutTask;
+ var stderr = await stdErrTask;
+
+ Assert.That(process.ExitCode, Is.EqualTo(0), $"Process exited with non-zero exit code {process.ExitCode}.{Environment.NewLine}StdOut:{Environment.NewLine}{stdout}{Environment.NewLine}StdErr:{Environment.NewLine}{stderr}");
+ Assert.That(stdout, Does.Contain("GPS:GPS Altitude: 201"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompare.Cli.Tests/SkiaSharpCompareCliTests.cs b/SkiaSharpCompare.Cli.Tests/SkiaSharpCompareCliTests.cs
new file mode 100644
index 0000000..84f03c1
--- /dev/null
+++ b/SkiaSharpCompare.Cli.Tests/SkiaSharpCompareCliTests.cs
@@ -0,0 +1,74 @@
+using NUnit.Framework;
+using System;
+using System.IO;
+
+namespace SkiaSharpCompare.Cli.Tests
+{
+ [TestFixture]
+ public class SkiaSharpCompareCliTests
+ {
+ [Test]
+ public void CompareFiles_NonImageFiles_AreReportedUnsupported()
+ {
+ var dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(dir);
+
+ try
+ {
+ var a = Path.Combine(dir, "a.txt");
+ var b = Path.Combine(dir, "b.txt");
+
+ File.WriteAllText(a, "not an image");
+ File.WriteAllText(b, "not an image");
+
+ var info = CliRunner.CompareFiles(a, b);
+ Assert.That(info.Unsupported, Is.True);
+ Assert.That(info.Result, Is.Null);
+ Assert.That(info.ErrorMessage, Is.Not.Null.And.Not.Empty);
+ }
+ finally
+ {
+ Directory.Delete(dir, true);
+ }
+ }
+
+ [Test]
+ public void CompareDirectories_MatchesMissingAndUnsupported()
+ {
+ var root = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
+ var dirA = Path.Combine(root, "A");
+ var dirB = Path.Combine(root, "B");
+ Directory.CreateDirectory(dirA);
+ Directory.CreateDirectory(dirB);
+
+ try
+ {
+ // file only in A
+ var onlyA = Path.Combine(dirA, "onlyA.txt");
+ File.WriteAllText(onlyA, "not image");
+
+ // file only in B
+ var onlyB = Path.Combine(dirB, "onlyB.txt");
+ File.WriteAllText(onlyB, "not image");
+
+ // common file (but not a valid image)
+ var commonA = Path.Combine(dirA, "common.jpg");
+ var commonB = Path.Combine(dirB, "common.jpg");
+ File.WriteAllText(commonA, "not an image");
+ File.WriteAllText(commonB, "not an image");
+
+ var summary = CliRunner.CompareDirectories(dirA, dirB);
+
+ Assert.That(summary.OnlyInA, Has.Member("onlyA.txt"));
+ Assert.That(summary.OnlyInB, Has.Member("onlyB.txt"));
+ Assert.That(summary.MatchedResults.Keys, Has.Member("common.jpg"));
+ Assert.That(summary.MatchedResults["common.jpg"], Is.Null);
+ Assert.That(summary.UnsupportedFiles, Has.Member("common.jpg"));
+ }
+ finally
+ {
+ Directory.Delete(root, true);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompare.Cli/CliRunner.cs b/SkiaSharpCompare.Cli/CliRunner.cs
new file mode 100644
index 0000000..716e15b
--- /dev/null
+++ b/SkiaSharpCompare.Cli/CliRunner.cs
@@ -0,0 +1,249 @@
+using Codeuctivity.SkiaSharpCompare;
+
+namespace SkiaSharpCompare.Cli
+{
+ ///
+ /// Helper that exposes programmatic entry points for the console and tests.
+ ///
+ public static class CliRunner
+ {
+ ///
+ /// Result wrapper for a single file comparison.
+ ///
+ public sealed class FileCompareInfo
+ {
+ public ICompareResult? Result { get; init; }
+ public bool Unsupported { get; init; }
+ public string? ErrorMessage { get; init; }
+ }
+
+ ///
+ /// Compare two image files. If an image cannot be loaded or compared,
+ /// will be true and will be null.
+ ///
+ public static FileCompareInfo CompareFiles(string pathA, string pathB, bool compareMetadata = true)
+ {
+ if (string.IsNullOrWhiteSpace(pathA))
+ {
+ throw new ArgumentException("PathA is required", nameof(pathA));
+ }
+
+ if (string.IsNullOrWhiteSpace(pathB))
+ {
+ throw new ArgumentException("PathB is required", nameof(pathB));
+ }
+
+ var absoluteA = Path.GetFullPath(pathA);
+ var absoluteB = Path.GetFullPath(pathB);
+
+ var comparer = new ImageCompare(compareMetadata: compareMetadata);
+
+ try
+ {
+ var result = comparer.CalcDiff(absoluteA, absoluteB);
+ return new FileCompareInfo
+ {
+ Result = result,
+ Unsupported = false,
+ ErrorMessage = null
+ };
+ }
+ catch (Exception ex)
+ {
+ return new FileCompareInfo
+ {
+ Result = null,
+ Unsupported = true,
+ ErrorMessage = ex.Message
+ };
+ }
+ }
+
+ ///
+ /// Compare two directories. Matches files by filename. Returns compare results for matched names,
+ /// lists of files existing only in one directory and a list of files which failed to be compared (unsupported).
+ ///
+ public static DirectoryCompareSummary CompareDirectories(string directoryA, string directoryB, bool compareMetadata = true)
+ {
+ if (string.IsNullOrWhiteSpace(directoryA))
+ {
+ throw new ArgumentException("directoryA is required", nameof(directoryA));
+ }
+
+ if (string.IsNullOrWhiteSpace(directoryB))
+ {
+ throw new ArgumentException("directoryB is required", nameof(directoryB));
+ }
+
+ var dirA = Path.GetFullPath(directoryA);
+ var dirB = Path.GetFullPath(directoryB);
+
+ if (!Directory.Exists(dirA))
+ {
+ throw new DirectoryNotFoundException(dirA);
+ }
+
+ if (!Directory.Exists(dirB))
+ {
+ throw new DirectoryNotFoundException(dirB);
+ }
+
+ var filesA = Directory.GetFiles(dirA).Select(Path.GetFileName).Where(n => n != null).Cast().ToHashSet(StringComparer.OrdinalIgnoreCase);
+ var filesB = Directory.GetFiles(dirB).Select(Path.GetFileName).Where(n => n != null).Cast().ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ var onlyInA = filesA.Except(filesB, StringComparer.OrdinalIgnoreCase).OrderBy(n => n).ToList();
+ var onlyInB = filesB.Except(filesA, StringComparer.OrdinalIgnoreCase).OrderBy(n => n).ToList();
+ var matched = filesA.Intersect(filesB, StringComparer.OrdinalIgnoreCase).OrderBy(n => n).ToList();
+
+ var matchedResults = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var unsupported = new List();
+
+ foreach (var fileName in matched)
+ {
+ var pathA = Path.Combine(dirA, fileName);
+ var pathB = Path.Combine(dirB, fileName);
+
+ var info = CompareFiles(pathA, pathB, compareMetadata: compareMetadata);
+ if (info.Unsupported)
+ {
+ matchedResults[fileName] = null;
+ unsupported.Add(fileName);
+ }
+ else
+ {
+ matchedResults[fileName] = info.Result;
+ }
+ }
+
+ return new DirectoryCompareSummary
+ {
+ MatchedResults = matchedResults,
+ OnlyInA = onlyInA,
+ OnlyInB = onlyInB,
+ UnsupportedFiles = unsupported
+ };
+ }
+
+ ///
+ /// Print a file comparison result to the provided text writer.
+ ///
+ public static void PrintCompareResult(FileCompareInfo info, TextWriter writer, string? nameA = null, string? nameB = null)
+ {
+ if (writer is null)
+ {
+ throw new ArgumentNullException(nameof(writer));
+ }
+
+ nameA ??= "A";
+ nameB ??= "B";
+
+ writer.WriteLine($"Comparing: {nameA} <> {nameB}");
+
+ if (info.Unsupported)
+ {
+ writer.WriteLine($" Unsupported or failed to compare: {info.ErrorMessage}");
+ return;
+ }
+
+ var result = info.Result!;
+ writer.WriteLine($" PixelErrorCount: {result.PixelErrorCount}");
+ writer.WriteLine($" PixelErrorPercentage: {result.PixelErrorPercentage:F4}");
+ writer.WriteLine($" AbsoluteError: {result.AbsoluteError}");
+ writer.WriteLine($" MeanError: {result.MeanError:F4}");
+
+ if (result.MetadataDifferences is null)
+ {
+ writer.WriteLine(" Metadata comparison disabled (null).");
+ }
+ else if (result.MetadataDifferences.Count == 0)
+ {
+ writer.WriteLine(" Metadata: no differences.");
+ }
+ else
+ {
+ writer.WriteLine(" Metadata differences:");
+ foreach (var kvp in result.MetadataDifferences)
+ {
+ writer.WriteLine($" {kvp.Key}: (A: {kvp.Value.ValueA ?? string.Empty}, B: {kvp.Value.ValueB ?? string.Empty})");
+ }
+ }
+ }
+
+ ///
+ /// Print directory compare summary to the writer.
+ ///
+ public static void PrintDirectorySummary(DirectoryCompareSummary summary, TextWriter writer)
+ {
+ if (writer is null)
+ {
+ throw new ArgumentNullException(nameof(writer));
+ }
+
+ writer.WriteLine("Directory comparison summary:");
+ writer.WriteLine();
+
+ writer.WriteLine("Matched files:");
+ if (summary.MatchedResults.Count == 0)
+ {
+ writer.WriteLine(" (none)");
+ }
+ else
+ {
+ foreach (var kvp in summary.MatchedResults.OrderBy(k => k.Key, StringComparer.OrdinalIgnoreCase))
+ {
+ writer.WriteLine($" {kvp.Key}:");
+ if (kvp.Value is null)
+ {
+ writer.WriteLine(" Unsupported or failed to compare");
+ }
+ else
+ {
+ writer.WriteLine($" PixelErrorCount: {kvp.Value.PixelErrorCount}, PixelErrorPercentage: {kvp.Value.PixelErrorPercentage:F4}");
+ }
+ }
+ }
+
+ writer.WriteLine();
+ writer.WriteLine("Only in A:");
+ if (summary.OnlyInA.Count == 0)
+ {
+ writer.WriteLine(" (none)");
+ }
+ else
+ {
+ foreach (var n in summary.OnlyInA)
+ {
+ writer.WriteLine($" {n}");
+ }
+ }
+
+ writer.WriteLine();
+ writer.WriteLine("Only in B:");
+ if (summary.OnlyInB.Count == 0)
+ {
+ writer.WriteLine(" (none)");
+ }
+ else
+ {
+ foreach (var n in summary.OnlyInB)
+ {
+ writer.WriteLine($" {n}");
+ }
+ }
+
+ writer.WriteLine();
+ writer.WriteLine("Unsupported files:");
+ if (summary.UnsupportedFiles.Count == 0)
+ {
+ writer.WriteLine(" (none)");
+ }
+ else
+ {
+ foreach (var n in summary.UnsupportedFiles)
+ {
+ writer.WriteLine($" {n}");
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompare.Cli/DirectoryCompareSummary.cs b/SkiaSharpCompare.Cli/DirectoryCompareSummary.cs
new file mode 100644
index 0000000..4cd2fd5
--- /dev/null
+++ b/SkiaSharpCompare.Cli/DirectoryCompareSummary.cs
@@ -0,0 +1,15 @@
+using Codeuctivity.SkiaSharpCompare;
+
+namespace SkiaSharpCompare.Cli
+{
+ ///
+ /// Summary of comparing two directories.
+ ///
+ public sealed class DirectoryCompareSummary
+ {
+ public Dictionary MatchedResults { get; init; } = new();
+ public List OnlyInA { get; init; } = new();
+ public List OnlyInB { get; init; } = new();
+ public List UnsupportedFiles { get; init; } = new();
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompare.Cli/Program.cs b/SkiaSharpCompare.Cli/Program.cs
new file mode 100644
index 0000000..ffad1d8
--- /dev/null
+++ b/SkiaSharpCompare.Cli/Program.cs
@@ -0,0 +1,81 @@
+using Codeuctivity.SkiaSharpCompare;
+
+namespace SkiaSharpCompare.Cli
+{
+ internal static class Program
+ {
+ private static int Main(string[] args)
+ {
+ if (args is null || args.Length < 1)
+ {
+ Console.WriteLine("Usage:");
+ Console.WriteLine(" dotnet run -- ");
+ Console.WriteLine(" dotnet run -- --dir");
+ Console.WriteLine(" dotnet run -- --meta # show metadata for a file");
+ return 2;
+ }
+
+ try
+ {
+ // If user requested metadata display
+ if (Array.IndexOf(args, "--meta") >= 0)
+ {
+ var fileArg = args[0];
+ var absolute = Path.GetFullPath(fileArg);
+ if (!File.Exists(absolute))
+ {
+ Console.Error.WriteLine($"File not found: {absolute}");
+ return 4;
+ }
+
+ try
+ {
+ var meta = MetadataExtractorAdapter.Extract(absolute);
+ Console.WriteLine($"Metadata for: {absolute}");
+ if (meta.Count == 0)
+ {
+ Console.WriteLine(" (no metadata found)");
+ }
+ else
+ {
+ foreach (var kv in meta.OrderBy(k => k.Key))
+ {
+ Console.WriteLine($" {kv.Key}: {kv.Value}");
+ }
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Failed to extract metadata: {ex.Message}");
+ return 5;
+ }
+ }
+
+ if (args.Length >= 3 && args[2] == "--dir")
+ {
+ var summary = CliRunner.CompareDirectories(args[0], args[1]);
+ CliRunner.PrintDirectorySummary(summary, Console.Out);
+ }
+ else if (args.Length >= 2)
+ {
+ var resultInfo = CliRunner.CompareFiles(args[0], args[1], compareMetadata: true);
+ CliRunner.PrintCompareResult(resultInfo, Console.Out, Path.GetFileName(args[0]), Path.GetFileName(args[1]));
+ }
+ else
+ {
+ Console.WriteLine("Insufficient arguments. See usage.");
+ return 2;
+ }
+
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Fatal error: {ex.Message}");
+ return 3;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompare.Cli/SkiaSharpCompare.Cli.csproj b/SkiaSharpCompare.Cli/SkiaSharpCompare.Cli.csproj
new file mode 100644
index 0000000..70af7c5
--- /dev/null
+++ b/SkiaSharpCompare.Cli/SkiaSharpCompare.Cli.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ 12
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/SkiaSharpCompare.sln b/SkiaSharpCompare.sln
deleted file mode 100644
index 1524ac7..0000000
--- a/SkiaSharpCompare.sln
+++ /dev/null
@@ -1,41 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.0.31903.59
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A1B6F6C3-ECBE-471C-AE18-199DEF5982C3}"
- ProjectSection(SolutionItems) = preProject
- .editorconfig = .editorconfig
- .github\workflows\cla.yml = .github\workflows\cla.yml
- .github\dependabot.yml = .github\dependabot.yml
- .github\workflows\dotnet.yml = .github\workflows\dotnet.yml
- README.md = README.md
- .github\workflows\stale.yml = .github\workflows\stale.yml
- EndProjectSection
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharpCompare", "SkiaSharpCompare\SkiaSharpCompare.csproj", "{556DA894-7456-4DE2-9E02-3815FE159D7F}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SkiaSharpCompareTestNunit", "SkiaSharpCompareTestNunit\SkiaSharpCompareTestNunit.csproj", "{9CFA9A6F-92FC-40E3-BB85-4C8F95EFC8C7}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {556DA894-7456-4DE2-9E02-3815FE159D7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {556DA894-7456-4DE2-9E02-3815FE159D7F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {556DA894-7456-4DE2-9E02-3815FE159D7F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {556DA894-7456-4DE2-9E02-3815FE159D7F}.Release|Any CPU.Build.0 = Release|Any CPU
- {9CFA9A6F-92FC-40E3-BB85-4C8F95EFC8C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {9CFA9A6F-92FC-40E3-BB85-4C8F95EFC8C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {9CFA9A6F-92FC-40E3-BB85-4C8F95EFC8C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {9CFA9A6F-92FC-40E3-BB85-4C8F95EFC8C7}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {AC72E02E-48E4-4DDE-B1FF-CEF86C9DFCAB}
- EndGlobalSection
-EndGlobal
diff --git a/SkiaSharpCompare.slnx b/SkiaSharpCompare.slnx
new file mode 100644
index 0000000..d19966e
--- /dev/null
+++ b/SkiaSharpCompare.slnx
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SkiaSharpCompare/CompareResult.cs b/SkiaSharpCompare/CompareResult.cs
index 30aac45..bed79df 100644
--- a/SkiaSharpCompare/CompareResult.cs
+++ b/SkiaSharpCompare/CompareResult.cs
@@ -1,45 +1,48 @@
+using System.Collections.Generic;
+
namespace Codeuctivity.SkiaSharpCompare
{
///
/// Dto - outcome of compared images
///
- public class CompareResult : ICompareResult
+ ///
+ /// ctor for CompareResult
+ ///
+ /// Mean error
+ /// Absolute error
+ /// Number of pixels that differ between images
+ /// Percentage of pixels that differ between images
+ /// Metadata that differ between images
+ public class CompareResult(int absoluteError, double meanError, int pixelErrorCount, double pixelErrorPercentage, Dictionary? metadataDifference = null) : ICompareResult
{
///
/// Mean pixel error of absolute pixel error
///
/// 0-765
- public double MeanError { get; }
+ public double MeanError { get; } = meanError;
///
/// Absolute pixel error, counts each color channel on every pixel the delta
///
- public int AbsoluteError { get; }
+ public int AbsoluteError { get; } = absoluteError;
///
/// Number of pixels that differ between images
///
- public int PixelErrorCount { get; }
+ public int PixelErrorCount { get; } = pixelErrorCount;
///
/// Percentage of pixels that differ between images
///
/// 0-100.0
- public double PixelErrorPercentage { get; }
+ public double PixelErrorPercentage { get; } = pixelErrorPercentage;
///
- /// ctor for CompareResult
+ /// Gets a collection of metadata keys and their differing values between two sources.
///
- /// Mean error
- /// Absolute error
- /// Number of pixels that differ between images
- /// Percentage of pixels that differ between images
- public CompareResult(int absoluteError, double meanError, int pixelErrorCount, double pixelErrorPercentage)
- {
- MeanError = meanError;
- AbsoluteError = absoluteError;
- PixelErrorCount = pixelErrorCount;
- PixelErrorPercentage = pixelErrorPercentage;
- }
+ /// Each entry in the dictionary represents a metadata key for which the values differ.
+ /// The tuple contains the value from the first source and the value from the second source, respectively. If a
+ /// value is null, the key may be missing from that source.
+ public Dictionary? MetadataDifferences { get; } = metadataDifference;
}
}
\ No newline at end of file
diff --git a/SkiaSharpCompare/CompareResultWithMetadata.cs b/SkiaSharpCompare/CompareResultWithMetadata.cs
new file mode 100644
index 0000000..abb087e
--- /dev/null
+++ b/SkiaSharpCompare/CompareResultWithMetadata.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+
+namespace Codeuctivity.SkiaSharpCompare
+{
+ internal class CompareResultWithMetadata(ICompareResult compareResult, Dictionary? metadataDiff) : ICompareResult
+ {
+ public double MeanError { get; } = compareResult.MeanError;
+ public int AbsoluteError { get; } = compareResult.AbsoluteError;
+ public int PixelErrorCount { get; } = compareResult.PixelErrorCount;
+ public double PixelErrorPercentage { get; } = compareResult.PixelErrorPercentage;
+ public Dictionary? MetadataDifferences { get; } = metadataDiff;
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompare/ICompareResult.cs b/SkiaSharpCompare/ICompareResult.cs
index 9d737ca..8099b25 100644
--- a/SkiaSharpCompare/ICompareResult.cs
+++ b/SkiaSharpCompare/ICompareResult.cs
@@ -1,4 +1,6 @@
-namespace Codeuctivity.SkiaSharpCompare
+using System.Collections.Generic;
+
+namespace Codeuctivity.SkiaSharpCompare
{
///
/// Dto - of compared images
@@ -24,5 +26,13 @@ public interface ICompareResult
/// Percentage of pixels that differ between images
///
double PixelErrorPercentage { get; }
+
+ ///
+ /// Gets a collection of metadata keys and their differing values between two sources.
+ ///
+ /// Each entry in the dictionary represents a metadata key for which the values differ
+ /// between the two compared sources. The tuple contains the value from the first source and the value from the
+ /// second source. If there are no differences or metadata is unavailable, the property returns null.
+ Dictionary? MetadataDifferences { get; }
}
}
\ No newline at end of file
diff --git a/SkiaSharpCompare/ImageCompare.cs b/SkiaSharpCompare/ImageCompare.cs
new file mode 100644
index 0000000..646a703
--- /dev/null
+++ b/SkiaSharpCompare/ImageCompare.cs
@@ -0,0 +1,336 @@
+using SkiaSharp;
+using System;
+using System.IO;
+
+namespace Codeuctivity.SkiaSharpCompare
+{
+ ///
+ /// Provides functionality for comparing images and calculating differences between them.
+ ///
+ /// The class allows users to compare images using various options,
+ /// such as resizing, transparency handling, and pixel color shift tolerance. It supports multiple input types,
+ /// including file paths, streams, and in-memory bitmaps. The class also provides methods to generate difference
+ /// masks and check for image equality or size equality.
+ ///
+ /// Initializes a new instance of the class with the specified options for resizing,
+ /// transparency handling, and color shift tolerance.
+ ///
+ /// Specifies how images should be resized before comparison. The default is .
+ /// Specifies how transparency should be handled during comparison. The default is .
+ /// Specifies the tolerance for color shifts in pixel values during comparison. A value of 0 means no
+ /// tolerance, and higher values allow for greater differences. The default is 0.
+ /// If true, compares image metadata (EXIF, etc.) in addition to pixel data.
+ public class ImageCompare(ResizeOption resizeOption = ResizeOption.DontResize, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel, int pixelColorShiftTolerance = 0, bool compareMetadata = false)
+ {
+ ///
+ /// Gets the resize option that determines how an image should be resized.
+ ///
+ public ResizeOption ResizeOption { get; } = resizeOption;
+
+ ///
+ /// Gets the transparency options that determine how transparency should be handled during image comparison.
+ ///
+ public TransparencyOptions TransparencyOptions { get; } = transparencyOptions;
+
+ ///
+ /// Gets the tolerance level for pixel color shifts in image processing operations.
+ ///
+ /// This property is typically used to determine whether two pixels are considered
+ /// similar in color during image comparison or analysis tasks.
+ public int PixelColorShiftTolerance { get; } = pixelColorShiftTolerance;
+
+ ///
+ /// Gets a value indicating whether metadata should be included in the comparison operation.
+ ///
+ public bool CompareMetadata { get; } = compareMetadata;
+
+ ///
+ /// Calculates the difference between two images located at the specified file paths.
+ ///
+ /// The comparison process may involve resizing the images or applying tolerances for
+ /// pixel color shifts and transparency, depending on the configured options.
+ /// The file path to the first image. This cannot be null or empty.
+ /// The file path to the second image. This cannot be null or empty.
+ /// An object representing the differences between the two images.
+ public ICompareResult CalcDiff(string pathImage1, string pathImage2)
+ {
+ var compareResult = Compare.CalcDiff(pathImage1, pathImage2, ResizeOption, PixelColorShiftTolerance, TransparencyOptions);
+
+ if (CompareMetadata)
+ {
+ var metadataDiff = MetadataComparer.CompareMetadata(pathImage1, pathImage2);
+ return new CompareResultWithMetadata(compareResult, metadataDiff);
+ }
+
+ return compareResult;
+ }
+
+ ///
+ /// Calculates the difference between two images located at the specified file paths, using a provided difference mask.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public ICompareResult CalcDiff(string pahtImage1, string pathImage2, string differenceMask)
+ {
+ var compareResult = Compare.CalcDiff(pahtImage1, pathImage2, differenceMask, ResizeOption, PixelColorShiftTolerance, TransparencyOptions);
+
+ if (CompareMetadata)
+ {
+ var metadataDiff = MetadataComparer.CompareMetadata(pahtImage1, pathImage2);
+ return new CompareResultWithMetadata(compareResult, metadataDiff);
+ }
+
+ return compareResult;
+ }
+
+ ///
+ /// Calculates the difference between two in-memory images represented as objects.
+ ///
+ ///
+ ///
+ ///
+ public ICompareResult CalcDiff(SKBitmap image1, SKBitmap image2)
+ {
+ ArgumentNullException.ThrowIfNull(image1);
+ ArgumentNullException.ThrowIfNull(image2);
+
+ if (CompareMetadata)
+ {
+ throw new NotSupportedException("Metadata comparison is not implemented for SKBitmap inputs. https://github.com/mono/SkiaSharp/issues/1139 Use the overload with streams or filepath to get support for metadata comparison.");
+ }
+
+ return Compare.CalcDiffInternal(image1, image2, null, ResizeOption, PixelColorShiftTolerance, TransparencyOptions);
+ }
+
+ ///
+ /// Calculates the difference between two in-memory images represented as objects.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public ICompareResult CalcDiff(SKBitmap image1, SKBitmap image2, SKBitmap differenceMaskPic)
+ {
+ if (CompareMetadata)
+ {
+ throw new NotSupportedException("Metadata comparison is not implemented for SKBitmap inputs. https://github.com/mono/SkiaSharp/issues/1139 Use the overload with streams or filepath to get support for metadata comparison.");
+ }
+
+ return Compare.CalcDiff(image1, image2, differenceMaskPic, ResizeOption, PixelColorShiftTolerance, TransparencyOptions);
+ }
+
+ ///
+ /// Calculates the difference between two in-memory images represented as objects.
+ ///
+ ///
+ ///
+ ///
+ public ICompareResult CalcDiff(Stream image1, Stream image2)
+ {
+ var compareResult = Compare.CalcDiff(image1, image2, ResizeOption, PixelColorShiftTolerance, TransparencyOptions);
+
+ if (CompareMetadata)
+ {
+ if (image1.CanSeek)
+ {
+ image1.Position = 0;
+ }
+
+ if (image2.CanSeek)
+ {
+ image2.Position = 0;
+ }
+
+ var metadataDiff = MetadataComparer.CompareMetadata(image1, image2);
+
+ if (image1.CanSeek)
+ {
+ image1.Position = 0;
+ }
+
+ if (image2.CanSeek)
+ {
+ image2.Position = 0;
+ }
+
+ return new CompareResultWithMetadata(compareResult, metadataDiff);
+ }
+
+ return compareResult;
+ }
+
+ ///
+ /// Calculates the difference between two images provided as file streams, using a specified mask image to focus the comparison.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public ICompareResult CalcDiff(Stream image1, Stream image2, SKBitmap maskImage)
+ {
+ var compareResult = Compare.CalcDiff(image1, image2, maskImage, ResizeOption, PixelColorShiftTolerance, TransparencyOptions);
+
+ if (CompareMetadata)
+ {
+ var metadataDiff = MetadataComparer.CompareMetadata(image1, image2);
+ return new CompareResultWithMetadata(compareResult, metadataDiff);
+ }
+
+ return compareResult;
+ }
+
+ ///
+ /// Calculates a difference mask image that highlights the differences between two images located at the specified file paths.
+ ///
+ ///
+ ///
+ ///
+ public SKBitmap CalcDiffMaskImage(string pathImage1, string pathImage2)
+ {
+ return Compare.CalcDiffMaskImage(pathImage1, pathImage2, ResizeOption, PixelColorShiftTolerance, TransparencyOptions);
+ }
+
+ ///
+ /// Calculates a difference mask image that highlights the differences between two in-memory images represented as objects.
+ ///
+ ///
+ ///
+ ///
+ public SKBitmap CalcDiffMaskImage(SKBitmap image1, SKBitmap image2)
+ {
+ return Compare.CalcDiffMaskImage(image1, image2, ResizeOption, PixelColorShiftTolerance, TransparencyOptions);
+ }
+
+ ///
+ /// Calculates a difference mask image that highlights the differences between two images located at the specified file paths,
+ ///
+ ///
+ ///
+ ///
+ ///
+ public SKBitmap CalcDiffMaskImage(string pathImage1, string pathImage2, string pathDiffMask)
+ {
+ return Compare.CalcDiffMaskImage(pathImage1, pathImage2, pathDiffMask, ResizeOption, PixelColorShiftTolerance, TransparencyOptions);
+ }
+
+ ///
+ /// Calculates a difference mask image that highlights the differences between two images provided as file streams.
+ ///
+ ///
+ ///
+ ///
+ public SKBitmap CalcDiffMaskImage(Stream image1, Stream image2)
+ {
+ return Compare.CalcDiffMaskImage(image1, image2, ResizeOption, PixelColorShiftTolerance, TransparencyOptions);
+ }
+
+ ///
+ /// Calculates a difference mask image that highlights the differences between two images provided as file streams,
+ ///
+ ///
+ ///
+ ///
+ ///
+ public SKBitmap CalcDiffMaskImage(Stream image1, Stream image2, Stream diffMask)
+ {
+ return Compare.CalcDiffMaskImage(image1, image2, diffMask, ResizeOption, PixelColorShiftTolerance, TransparencyOptions);
+ }
+
+ ///
+ /// Calculates a difference mask image that highlights the differences between two in-memory images represented as objects,
+ ///
+ ///
+ ///
+ ///
+ ///
+ public SKBitmap CalcDiffMaskImage(SKBitmap image1, SKBitmap image2, SKBitmap diffMask)
+ {
+ return Compare.CalcDiffMaskImage(image1, image2, diffMask, ResizeOption, PixelColorShiftTolerance, TransparencyOptions);
+ }
+
+ ///
+ /// Checks if two images located at the specified file paths are identical, considering the configured options for resizing,
+ ///
+ ///
+ ///
+ ///
+ public bool ImagesAreEqual(string pathImage1, string pathImage2)
+ {
+ return Compare.ImagesAreEqual(pathImage1, pathImage2, ResizeOption, PixelColorShiftTolerance, TransparencyOptions, CompareMetadata);
+ }
+
+ ///
+ /// Checks if two images provided as file streams are identical, considering the configured options for resizing,
+ ///
+ ///
+ ///
+ ///
+ public bool ImagesAreEqual(Stream image1, Stream image2)
+ {
+ return Compare.ImagesAreEqual(image1, image2, ResizeOption, PixelColorShiftTolerance, TransparencyOptions, CompareMetadata);
+ }
+
+ ///
+ /// Checks if two in-memory images represented as objects are identical, considering the configured options for resizing,
+ ///
+ ///
+ ///
+ ///
+ public bool ImagesAreEqual(SKBitmap image1, SKBitmap image2)
+ {
+ return Compare.ImagesAreEqual(image1, image2, ResizeOption, PixelColorShiftTolerance, TransparencyOptions, CompareMetadata);
+ }
+
+ ///
+ /// Checks if two images have the same dimensions (width and height).
+ ///
+ ///
+ ///
+ ///
+ public static bool ImagesHaveEqualSize(string pathImage1, string pathImage2)
+ {
+ return Compare.ImagesHaveEqualSize(pathImage1, pathImage2);
+ }
+
+ ///
+ /// Create a tolerance mask by expanding non-black regions in a diff mask.
+ /// Non-black pixels in diffMask will be grown by padding into broader white areas
+ /// to tolerate local differences (e.g., filenames, timestamps).
+ ///
+ public SKBitmap CreateToleranceMaskFromDiff(SKBitmap diffMask, int padding = 10)
+ {
+ ArgumentNullException.ThrowIfNull(diffMask);
+ var w = diffMask.Width;
+ var h = diffMask.Height;
+ var mask = new SKBitmap(w, h, SKColorType.Rgba8888, SKAlphaType.Unpremul);
+ using var canvas = new SKCanvas(mask);
+ canvas.Clear(SKColors.Black);
+
+ using var paint = new SKPaint { Color = SKColors.White, IsAntialias = false };
+
+ for (var y = 0; y < h; y++)
+ {
+ for (var x = 0; x < w; x++)
+ {
+ var col = diffMask.GetPixel(x, y);
+ if (col.Alpha > 16 && (col.Red > 16 || col.Green > 16 || col.Blue > 16))
+ {
+ var left = Math.Max(0, x - padding);
+ var top = Math.Max(0, y - padding);
+ var right = Math.Min(w, x + padding + 1);
+ var bottom = Math.Min(h, y + padding + 1);
+ var rect = new SKRectI(left, top, right, bottom);
+ canvas.DrawRect(rect, paint);
+ }
+ }
+ }
+
+ canvas.Flush();
+ return mask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompare/MetadataComparer.cs b/SkiaSharpCompare/MetadataComparer.cs
new file mode 100644
index 0000000..5869b5b
--- /dev/null
+++ b/SkiaSharpCompare/MetadataComparer.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Codeuctivity.SkiaSharpCompare
+{
+ internal static class MetadataComparer
+ {
+ internal static Dictionary? CompareMetadata(string pathImageActual, string pathImageExpected)
+ {
+ var metaA = MetadataExtractorAdapter.Extract(pathImageActual);
+ var metaB = MetadataExtractorAdapter.Extract(pathImageExpected);
+
+ return GetMetadataDifferences(metaA, metaB);
+ }
+
+ internal static Dictionary? CompareMetadata(Stream imageActual, Stream imageExpected)
+ {
+ var metaA = MetadataExtractorAdapter.Extract(imageActual);
+ var metaB = MetadataExtractorAdapter.Extract(imageExpected);
+
+ return GetMetadataDifferences(metaA, metaB);
+ }
+
+ private static Dictionary? GetMetadataDifferences(IReadOnlyDictionary a, IReadOnlyDictionary b)
+ {
+ var diffs = new List<(string, string?, string?)>();
+ var allKeys = new HashSet(a.Keys, StringComparer.OrdinalIgnoreCase);
+ allKeys.UnionWith(b.Keys);
+
+ foreach (var key in allKeys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase))
+ {
+ a.TryGetValue(key, out var valA);
+ b.TryGetValue(key, out var valB);
+
+ // Normalize whitespace for comparison
+ var normA = valA?.Trim() ?? string.Empty;
+ var normB = valB?.Trim() ?? string.Empty;
+
+ if (!string.Equals(normA, normB, StringComparison.Ordinal))
+ {
+ diffs.Add((key, valA, valB));
+ }
+ }
+
+ return diffs.ToDictionary(diff => diff.Item1, diff => (diff.Item2, diff.Item3)
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompare/MetadataExtractorAdapter.cs b/SkiaSharpCompare/MetadataExtractorAdapter.cs
new file mode 100644
index 0000000..6a9fb81
--- /dev/null
+++ b/SkiaSharpCompare/MetadataExtractorAdapter.cs
@@ -0,0 +1,76 @@
+using MetadataExtractor;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Codeuctivity.SkiaSharpCompare
+{
+ ///
+ /// Provides static methods for extracting image metadata and returning it as a normalized dictionary of tag names
+ /// and values.
+ ///
+ /// This class serves as an adapter for reading metadata from image files or streams using the
+ /// underlying MetadataExtractor library. All returned tag keys are normalized in the format "Directory:TagName" for
+ /// consistency. The class is thread-safe as it contains only stateless static methods.
+ public static class MetadataExtractorAdapter
+ {
+ ///
+ /// Extracts metadata tags from the specified image stream and returns them as a normalized, case-insensitive
+ /// dictionary.
+ ///
+ /// The caller is responsible for ensuring that the provided stream is positioned at the
+ /// start of the image data and remains open for the duration of the operation. The returned dictionary uses
+ /// case-insensitive keys. If the stream is not seekable, consider passing a seekable copy to avoid
+ /// errors.
+ /// A seekable stream containing image data from which to read metadata. The stream must support reading and
+ /// seeking.
+ /// A read-only dictionary containing metadata tag names and their corresponding values extracted from the
+ /// image. The dictionary is empty if no metadata is found.
+ public static IReadOnlyDictionary Extract(Stream imageStream)
+ {
+ var map = new Dictionary(System.StringComparer.OrdinalIgnoreCase);
+
+ // MetadataExtractor reads from stream
+ var directories = ImageMetadataReader.ReadMetadata(imageStream);
+ return CreateResult(map, directories);
+ }
+
+ ///
+ /// Extracts metadata from the specified image file and returns it as a read-only dictionary of tag names and
+ /// values.
+ ///
+ /// The path to the image file from which to extract metadata. Cannot be null or empty.
+ /// A read-only dictionary containing metadata tag names and their corresponding values. The dictionary is empty
+ /// if no metadata is found.
+ public static IReadOnlyDictionary Extract(string imagePath)
+ {
+ var map = new Dictionary(System.StringComparer.OrdinalIgnoreCase);
+
+ // MetadataExtractor reads from stream
+ var directories = ImageMetadataReader.ReadMetadata(imagePath);
+ return CreateResult(map, directories);
+ }
+
+ private static Dictionary CreateResult(Dictionary map, IReadOnlyList directories)
+ {
+ foreach (var directory in directories)
+ {
+ foreach (var tag in directory.Tags)
+ {
+ // Normalize key as "Directory:TagName"
+ var key = $"{directory.Name}:{tag.Name}";
+ if (map.TryGetValue(key, out var existingValue))
+ {
+ // If duplicate keys occur, append with a separator
+ map[key] = existingValue + "; " + (tag.Description ?? string.Empty);
+ }
+ else
+ {
+ map[key] = tag.Description ?? string.Empty;
+ }
+ }
+ }
+
+ return map;
+ }
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompare/ResizeOption.cs b/SkiaSharpCompare/ResizeOption.cs
new file mode 100644
index 0000000..ed31140
--- /dev/null
+++ b/SkiaSharpCompare/ResizeOption.cs
@@ -0,0 +1,18 @@
+namespace Codeuctivity.SkiaSharpCompare
+{
+ ///
+ /// Options that are applied if images do have different image dimension
+ ///
+ public enum ResizeOption
+ {
+ ///
+ /// Dont resize images with different size.
+ ///
+ DontResize,
+
+ ///
+ /// Images with different size will get resized before pixel based compare is used to determine equality.
+ ///
+ Resize
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompare/SkiaSharpCompare.cs b/SkiaSharpCompare/SkiaSharpCompare.cs
index 62749c0..cc16dd8 100644
--- a/SkiaSharpCompare/SkiaSharpCompare.cs
+++ b/SkiaSharpCompare/SkiaSharpCompare.cs
@@ -1,5 +1,6 @@
using SkiaSharp;
using System;
+using System.Collections.Generic;
using System.IO;
namespace Codeuctivity.SkiaSharpCompare
@@ -7,238 +8,398 @@ namespace Codeuctivity.SkiaSharpCompare
///
/// SkiaSharpCompare, compares images. An alpha channel is ignored.
///
- public static class SkiaSharpCompare
+ public static class Compare
{
private const string sizeDiffersExceptionMessage = "Size of images differ.";
///
/// Is true if width and height of both images are equal
///
- ///
- ///
+ ///
+ ///
///
- public static bool ImagesHaveEqualSize(string pathImageActual, string pathImageExpected)
+ public static bool ImagesHaveEqualSize(string pathImage1, string pathImage2)
{
- using var actualImage = SKBitmap.Decode(pathImageActual);
- using var expectedImage = SKBitmap.Decode(pathImageExpected);
- return ImagesHaveEqualSize(actualImage, expectedImage);
+ using var image1 = SKBitmap.Decode(pathImage1);
+ using var image2 = SKBitmap.Decode(pathImage2);
+ return ImagesHaveEqualSize(image1, image2);
}
///
/// Is true if width and height of both images are equal
///
- ///
- ///
+ ///
+ ///
///
- public static bool ImagesHaveEqualSize(Stream actual, Stream expected)
+ public static bool ImagesHaveEqualSize(Stream image1, Stream image2)
{
- using var actualImage = SKBitmap.Decode(actual);
- using var expectedImage = SKBitmap.Decode(expected);
+ ArgumentNullException.ThrowIfNull(image1);
+ ArgumentNullException.ThrowIfNull(image2);
+ using var actualImage = DecodeStream(image1);
+ using var expectedImage = DecodeStream(image2);
return ImagesHaveEqualSize(actualImage, expectedImage);
}
///
/// Is true if width and height of both images are equal
///
- ///
- ///
+ ///
+ ///
///
- public static bool ImagesHaveEqualSize(SKBitmap actualImage, SKBitmap expectedImage)
+ public static bool ImagesHaveEqualSize(SKBitmap image1, SKBitmap image2)
{
- return ImagesHaveSameDimension(actualImage, expectedImage);
+ return ImagesHaveSameDimension(image1, image2);
}
///
/// Compares two images for equivalence
///
- ///
- ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// If true, compares image metadata (EXIF, etc.) in addition to pixel data.
/// True if every pixel of actual is equal to expected
- public static bool ImagesAreEqual(string pathImageActual, string pathImageExpected)
+ public static bool ImagesAreEqual(string pathImage1, string pathImage2, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel, bool compareMetadata = false)
{
- using var actualImage = SKBitmap.Decode(pathImageActual);
- using var expectedImage = SKBitmap.Decode(pathImageExpected);
- return ImagesAreEqual(actualImage, expectedImage);
+ if (compareMetadata)
+ {
+ var hasMetadataDiff = MetadataComparer.CompareMetadata(pathImage1, pathImage2)?.Count != 0;
+
+ if (hasMetadataDiff)
+ {
+ return false;
+ }
+ }
+
+ using var image1 = SKBitmap.Decode(pathImage1);
+ using var image2 = SKBitmap.Decode(pathImage2);
+ return ImagesAreEqual(image1, image2, resizeOption, pixelColorShiftTolerance, transparencyOptions);
}
///
/// Compares two images for equivalence
///
- ///
- ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// If true, compares image metadata (EXIF, etc.) in addition to pixel data.
/// True if every pixel of actual is equal to expected
- public static bool ImagesAreEqual(Stream actual, Stream expected)
+ public static bool ImagesAreEqual(Stream image1, Stream image2, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel, bool compareMetadata = false)
{
- using var actualImage = SKBitmap.Decode(actual);
- using var expectedImage = SKBitmap.Decode(expected);
- return ImagesAreEqual(actualImage, expectedImage);
+ if (compareMetadata)
+ {
+ var hasMetadataDiff = MetadataComparer.CompareMetadata(image1, image2)?.Count != 0;
+
+ if (hasMetadataDiff)
+ {
+ return false;
+ }
+ }
+
+ ArgumentNullException.ThrowIfNull(image1);
+ ArgumentNullException.ThrowIfNull(image2);
+
+ using var decodedImage1 = DecodeStream(image1);
+ using var decodedImage2 = DecodeStream(image2);
+ return ImagesAreEqual(decodedImage1, decodedImage2, resizeOption, pixelColorShiftTolerance, transparencyOptions);
}
///
/// Compares two images for equivalence
///
- ///
- ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// If true, compares image metadata (EXIF, etc.) in addition to pixel data.
/// True if every pixel of actual is equal to expected
- public static bool ImagesAreEqual(SKBitmap actual, SKBitmap expected)
+ public static bool ImagesAreEqual(SKBitmap image1, SKBitmap image2, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel, bool compareMetadata = false)
{
- if (!ImagesHaveSameDimension(actual, expected))
+ if (compareMetadata)
+ {
+ throw new NotSupportedException("Metadata comparison is not implemented for SKBitmap inputs. https://github.com/mono/SkiaSharp/issues/1139 Use the overload with streams or filepath to get support for metadata comparison.");
+ }
+
+ ArgumentNullException.ThrowIfNull(image1);
+ ArgumentNullException.ThrowIfNull(image2);
+
+ if (resizeOption == ResizeOption.DontResize && !ImagesHaveSameDimension(image1, image2))
{
return false;
}
- for (var x = 0; x < actual.Width; x++)
+ if (resizeOption == ResizeOption.DontResize || ImagesHaveSameDimension(image1, image2))
{
- for (var y = 0; y < actual.Height; y++)
+ for (var x = 0; x < image1.Width; x++)
{
- if (!actual.GetPixel(x, y).Equals(expected.GetPixel(x, y)))
+ for (var y = 0; y < image1.Height; y++)
{
- return false;
+ if (transparencyOptions == TransparencyOptions.CompareAlphaChannel && pixelColorShiftTolerance == 0 && !image1.GetPixel(x, y).Equals(image2.GetPixel(x, y)))
+ {
+ return false;
+ }
+ else
+ {
+ var actualPixel = image1.GetPixel(x, y);
+ var expectedPixel = image2.GetPixel(x, y);
+ var a = 0;
+
+ if (transparencyOptions == TransparencyOptions.CompareAlphaChannel)
+ {
+ a = Math.Abs(expectedPixel.Alpha - actualPixel.Alpha);
+ }
+
+ var r = Math.Abs(expectedPixel.Red - actualPixel.Red);
+ var g = Math.Abs(expectedPixel.Green - actualPixel.Green);
+ var b = Math.Abs(expectedPixel.Blue - actualPixel.Blue);
+ var sum = r + g + b + a;
+
+ if (sum > pixelColorShiftTolerance)
+ {
+ return false;
+ }
+ }
}
}
+
+ return true;
+ }
+
+ var grown = GrowToSameDimension(image1, image2);
+ try
+ {
+ return ImagesAreEqual(grown.Item1, grown.Item2, ResizeOption.DontResize);
+ }
+ finally
+ {
+ grown.Item1?.Dispose();
+ grown.Item2?.Dispose();
}
+ }
- return true;
+ ///
+ /// Calculates ICompareResult expressing the amount of difference of both images
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Mean and absolute pixel error
+ public static ICompareResult CalcDiff(string pathImage1, string pathImage2, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.IgnoreAlphaChannel)
+ {
+ using var actual = SKBitmap.Decode(pathImage1);
+ using var expected = SKBitmap.Decode(pathImage2);
+ return CalcDiffInternal(actual, expected, null, resizeOption, pixelColorShiftTolerance, transparencyOptions);
}
///
/// Calculates ICompareResult expressing the amount of difference of both images
///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Mean and absolute pixel error
+ public static ICompareResult CalcDiff(Stream image1, Stream image2, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.IgnoreAlphaChannel)
+ {
+ ArgumentNullException.ThrowIfNull(image1);
+ ArgumentNullException.ThrowIfNull(image2);
+
+ using var actual = DecodeStream(image1);
+ using var expected = DecodeStream(image2);
+ return CalcDiffInternal(actual, expected, null, resizeOption, pixelColorShiftTolerance, transparencyOptions);
+ }
+
+ ///
+ /// Calculates ICompareResult expressing the amount of difference of both images using a mask image for tolerated difference between the two images
+ ///
///
///
+ ///
+ ///
+ ///
+ ///
/// Mean and absolute pixel error
- public static ICompareResult CalcDiff(string pathActualImage, string pathExpectedImage)
+ public static ICompareResult CalcDiff(string pathActualImage, string pathExpectedImage, string pathMaskImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel)
{
- using var actual = SKBitmap.Decode(pathActualImage);
- using var expected = SKBitmap.Decode(pathExpectedImage);
- return CalcDiff(actual, expected);
+ using var image1 = SKBitmap.Decode(pathActualImage);
+ using var image2 = SKBitmap.Decode(pathExpectedImage);
+ using var mask = SKBitmap.Decode(pathMaskImage);
+ return CalcDiff(image1, image2, mask, resizeOption, pixelColorShiftTolerance, transparencyOptions);
}
///
/// Calculates ICompareResult expressing the amount of difference of both images
///
- ///
- ///
- /// Mean and absolute pixel error
- public static ICompareResult CalcDiff(Stream actualImage, Stream expectedImage)
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static ICompareResult CalcDiff(Stream image1, Stream image2, SKBitmap maskImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel)
{
- using var actual = SKBitmap.Decode(actualImage);
- using var expected = SKBitmap.Decode(expectedImage);
- return CalcDiff(actual, expected);
+ ArgumentNullException.ThrowIfNull(image1);
+ ArgumentNullException.ThrowIfNull(image2);
+ ArgumentNullException.ThrowIfNull(maskImage);
+
+ using var actual = DecodeStream(image1);
+ using var expected = DecodeStream(image2);
+ return CalcDiff(actual, expected, maskImage, resizeOption, pixelColorShiftTolerance, transparencyOptions);
+ }
+
+ ///
+ /// Compares two images for equivalence
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static ICompareResult CalcDiff(SKBitmap image1, SKBitmap image2, SKBitmap maskImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel)
+ {
+ ArgumentNullException.ThrowIfNull(image1);
+ ArgumentNullException.ThrowIfNull(image2);
+ ArgumentNullException.ThrowIfNull(maskImage);
+
+ var metadataDifference = new Dictionary();
+ return CalcDiff(image1, image2, maskImage, metadataDifference, resizeOption, pixelColorShiftTolerance, transparencyOptions);
}
///
/// Calculates ICompareResult expressing the amount of difference of both images
///
- ///
- ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
/// Mean and absolute pixel error
- public static ICompareResult CalcDiff(SKBitmap actual, SKBitmap expected)
+ internal static ICompareResult CalcDiffInternal(SKBitmap image1, SKBitmap image2, Dictionary? metadataDifference, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.IgnoreAlphaChannel)
{
- if (!ImagesHaveSameDimension(actual, expected))
+ var imagesHaveSameDimension = ImagesHaveSameDimension(image1, image2);
+
+ if (resizeOption == ResizeOption.Resize && !imagesHaveSameDimension)
+ {
+ var grown = GrowToSameDimension(image1, image2);
+ try
+ {
+ return CalcDiffInternal(grown.Item1, grown.Item2, metadataDifference, ResizeOption.DontResize, pixelColorShiftTolerance, transparencyOptions);
+ }
+ finally
+ {
+ grown.Item1?.Dispose();
+ grown.Item2?.Dispose();
+ }
+ }
+
+ if (!ImagesHaveSameDimension(image1, image2))
{
throw new SkiaSharpCompareException(sizeDiffersExceptionMessage);
}
- var quantity = actual.Width * actual.Height;
+ var quantity = image1.Width * image1.Height;
var absoluteError = 0;
var pixelErrorCount = 0;
- for (var x = 0; x < actual.Width; x++)
+ for (var x = 0; x < image1.Width; x++)
{
- for (var y = 0; y < actual.Height; y++)
+ for (var y = 0; y < image1.Height; y++)
{
- var actualPixel = actual.GetPixel(x, y);
- var expectedPixel = expected.GetPixel(x, y);
+ var actualPixel = image1.GetPixel(x, y);
+ var expectedPixel = image2.GetPixel(x, y);
var r = Math.Abs(expectedPixel.Red - actualPixel.Red);
var g = Math.Abs(expectedPixel.Green - actualPixel.Green);
var b = Math.Abs(expectedPixel.Blue - actualPixel.Blue);
- absoluteError = absoluteError + r + g + b;
-
- pixelErrorCount += r + g + b > 0 ? 1 : 0;
+ var sum = r + g + b;
+ if (transparencyOptions == TransparencyOptions.CompareAlphaChannel)
+ {
+ var a = Math.Abs(expectedPixel.Alpha - actualPixel.Alpha);
+ sum += a;
+ }
+ absoluteError += (sum > pixelColorShiftTolerance ? sum : 0);
+ pixelErrorCount += (sum > pixelColorShiftTolerance) ? 1 : 0;
}
}
var meanError = (double)absoluteError / quantity;
var pixelErrorPercentage = (double)pixelErrorCount / quantity * 100;
- return new CompareResult(absoluteError, meanError, pixelErrorCount, pixelErrorPercentage);
+ return new CompareResult(absoluteError, meanError, pixelErrorCount, pixelErrorPercentage, metadataDifference);
}
- ///
- /// Calculates ICompareResult expressing the amount of difference of both images using a mask image for tolerated difference between the two images
- ///
- ///
- ///
- ///
- /// Mean and absolute pixel error
- public static ICompareResult CalcDiff(string pathActualImage, string pathExpectedImage, string pathMaskImage)
+ private static SKBitmap DecodeStream(Stream stream)
{
- using var actual = SKBitmap.Decode(pathActualImage);
- using var expected = SKBitmap.Decode(pathExpectedImage);
- using var mask = SKBitmap.Decode(pathMaskImage);
- return CalcDiff(actual, expected, mask);
- }
+ // Addressing https://github.com/mono/SkiaSharp/issues/2263
- ///
- /// Calculates ICompareResult expressing the amount of difference of both images
- ///
- ///
- ///
- ///
- ///
- public static ICompareResult CalcDiff(Stream actualImage, Stream expectedImage, SKBitmap maskImage)
- {
- using var actual = SKBitmap.Decode(actualImage);
- using var expected = SKBitmap.Decode(expectedImage);
- return CalcDiff(actual, expected, maskImage);
- }
+ using var copy = new MemoryStream();
- private static bool ImagesHaveSameDimension(SKBitmap actual, SKBitmap expected)
- {
- if (actual == null)
+ if (stream.CanSeek)
{
- throw new ArgumentNullException(nameof(actual));
+ stream.Position = 0;
}
- if (expected == null)
+ stream.CopyTo(copy);
+
+ if (stream.CanSeek)
{
- throw new ArgumentNullException(nameof(expected));
+ stream.Position = 0;
}
- return actual.Height == expected.Height && actual.Width == expected.Width;
+ var bytes = copy.ToArray();
+ using var skData = SKData.CreateCopy(bytes);
+
+ return SKBitmap.Decode(skData);
}
- ///
- /// Calculates ICompareResult expressing the amount of difference of both images using a image mask for tolerated difference between the two images
- ///
- ///
- ///
- ///
- /// Mean and absolute pixel error
- public static ICompareResult CalcDiff(SKBitmap actual, SKBitmap expected, SKBitmap maskImage)
+ internal static ICompareResult CalcDiff(SKBitmap image1, SKBitmap image2, SKBitmap maskImage, Dictionary metadataDifference, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel)
{
- if (!ImagesHaveSameDimension(actual, expected))
+ ArgumentNullException.ThrowIfNull(maskImage);
+
+ var imagesHaveSameDimension = ImagesHaveSameDimension(image1, image2) && ImagesHaveSameDimension(image1, maskImage);
+
+ if (resizeOption == ResizeOption.Resize && !imagesHaveSameDimension)
{
- throw new SkiaSharpCompareException(sizeDiffersExceptionMessage);
+ var grown = GrowToSameDimension(image1, image2, maskImage);
+ try
+ {
+ return CalcDiff(grown.Item1, grown.Item2, grown.Item3, ResizeOption.DontResize, pixelColorShiftTolerance, transparencyOptions);
+ }
+ finally
+ {
+ grown.Item1?.Dispose();
+ grown.Item2?.Dispose();
+ grown.Item3?.Dispose();
+ }
}
- if (maskImage == null)
+ if (!imagesHaveSameDimension)
{
- throw new ArgumentNullException(nameof(maskImage));
+ throw new SkiaSharpCompareException(sizeDiffersExceptionMessage);
}
- var quantity = actual.Width * actual.Height;
+ var quantity = image1.Width * image1.Height;
var absoluteError = 0;
var pixelErrorCount = 0;
- for (var x = 0; x < actual.Width; x++)
+ for (var x = 0; x < image1.Width; x++)
{
- for (var y = 0; y < actual.Height; y++)
+ for (var y = 0; y < image1.Height; y++)
{
var maskImagePixel = maskImage.GetPixel(x, y);
- var actualPixel = actual.GetPixel(x, y);
- var expectedPixel = expected.GetPixel(x, y);
+ var actualPixel = image1.GetPixel(x, y);
+ var expectedPixel = image2.GetPixel(x, y);
var r = Math.Abs(expectedPixel.Red - actualPixel.Red);
var g = Math.Abs(expectedPixel.Green - actualPixel.Green);
@@ -261,87 +422,349 @@ public static ICompareResult CalcDiff(SKBitmap actual, SKBitmap expected, SKBitm
error += b;
}
- absoluteError += error;
- pixelErrorCount += error > 0 ? 1 : 0;
+ if (transparencyOptions == TransparencyOptions.CompareAlphaChannel)
+ {
+ var a = Math.Abs(expectedPixel.Alpha - actualPixel.Alpha);
+ if (a > maskImagePixel.Alpha)
+ {
+ error += a;
+ }
+ }
+
+ absoluteError += (error > pixelColorShiftTolerance ? error : 0);
+ pixelErrorCount += error > pixelColorShiftTolerance ? 1 : 0;
}
}
var meanError = (double)absoluteError / quantity;
var pixelErrorPercentage = (double)pixelErrorCount / quantity * 100;
- return new CompareResult(absoluteError, meanError, pixelErrorCount, pixelErrorPercentage);
+ return new CompareResult(absoluteError, meanError, pixelErrorCount, pixelErrorPercentage, metadataDifference);
+ }
+
+ private static bool ImagesHaveSameDimension(SKBitmap actual, SKBitmap expected)
+ {
+ ArgumentNullException.ThrowIfNull(actual);
+ ArgumentNullException.ThrowIfNull(expected);
+
+ return actual.Height == expected.Height && actual.Width == expected.Width;
}
///
/// Creates a diff mask image of two images
///
- ///
- ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Image representing diff, black means no diff between actual image and expected image, white means max diff
+ public static SKBitmap CalcDiffMaskImage(string pathImage1, string pathImage2, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel)
+ {
+ using var actual = SKBitmap.Decode(pathImage1);
+ using var expected = SKBitmap.Decode(pathImage2);
+ return CalcDiffMaskImage(actual, expected, resizeOption, pixelColorShiftTolerance, transparencyOptions);
+ }
+
+ ///
+ /// Creates a diff mask image of two images
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Image representing diff, black means no diff between actual image and expected image, white means max diff
+ public static SKBitmap CalcDiffMaskImage(string pathImage1, string pathImage2, string pathMaskImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel)
+ {
+ using var actual = SKBitmap.Decode(pathImage1);
+ using var expected = SKBitmap.Decode(pathImage2);
+ using var mask = SKBitmap.Decode(pathMaskImage);
+ return CalcDiffMaskImage(actual, expected, mask, resizeOption, pixelColorShiftTolerance, transparencyOptions);
+ }
+
+ ///
+ /// Creates a diff mask image of two images
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Image representing diff, black means no diff between actual image and expected image, white means max diff
+ public static SKBitmap CalcDiffMaskImage(Stream image1, Stream image2, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel)
+ {
+ ArgumentNullException.ThrowIfNull(image1);
+ ArgumentNullException.ThrowIfNull(image2);
+
+ using var actual = DecodeStream(image1);
+ using var expected = DecodeStream(image2);
+
+ return CalcDiffMaskImage(actual, expected, resizeOption, pixelColorShiftTolerance, transparencyOptions);
+ }
+
+ ///
+ /// Creates a diff mask image of two images
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
/// Image representing diff, black means no diff between actual image and expected image, white means max diff
- public static SKBitmap CalcDiffMaskImage(string pathActualImage, string pathExpectedImage)
+ public static SKBitmap CalcDiffMaskImage(Stream image1, Stream image2, Stream maskImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel)
{
- using var actual = SKBitmap.Decode(pathActualImage);
- using var expected = SKBitmap.Decode(pathExpectedImage);
- return CalcDiffMaskImage(actual, expected);
+ ArgumentNullException.ThrowIfNull(image1);
+ ArgumentNullException.ThrowIfNull(image2);
+ ArgumentNullException.ThrowIfNull(maskImage);
+
+ using var actual = DecodeStream(image1);
+ using var expected = DecodeStream(image2);
+ using var mask = DecodeStream(maskImage);
+
+ return CalcDiffMaskImage(actual, expected, mask, resizeOption, pixelColorShiftTolerance, transparencyOptions);
}
///
/// Creates a diff mask image of two images
///
- ///
- ///
+ ///
+ ///
+ ///
+ ///
+ ///
/// Image representing diff, black means no diff between actual image and expected image, white means max diff
- public static SKBitmap CalcDiffMaskImage(Stream actualImage, Stream expectedImage)
+ public static SKBitmap CalcDiffMaskImage(SKBitmap image1, SKBitmap image2, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel)
{
- if (actualImage.CanSeek)
+ var imagesHAveSameDimension = ImagesHaveSameDimension(image1, image2);
+
+ if (resizeOption == ResizeOption.DontResize && !imagesHAveSameDimension)
{
- actualImage.Position = 0;
+ throw new SkiaSharpCompareException(sizeDiffersExceptionMessage);
}
- if (expectedImage.CanSeek)
+
+ if (imagesHAveSameDimension)
{
- expectedImage.Position = 0;
+ var maskImage = new SKBitmap(image1.Width, image1.Height);
+
+ for (var x = 0; x < image1.Width; x++)
+ {
+ for (var y = 0; y < image1.Height; y++)
+ {
+ var actualPixel = image1.GetPixel(x, y);
+ var expectedPixel = image2.GetPixel(x, y);
+
+ var red = (byte)Math.Abs(actualPixel.Red - expectedPixel.Red);
+ var green = (byte)Math.Abs(actualPixel.Green - expectedPixel.Green);
+ var blue = (byte)Math.Abs(actualPixel.Blue - expectedPixel.Blue);
+
+ if (pixelColorShiftTolerance == 0)
+ {
+ if (transparencyOptions == TransparencyOptions.CompareAlphaChannel)
+ {
+ var alpha = (byte)Math.Abs(actualPixel.Alpha - expectedPixel.Alpha);
+ // Ensure mask pixel has full opacity if there's any color difference
+ var effectiveAlpha = (red > 0 || green > 0 || blue > 0) ? (byte)255 : alpha;
+ var pixel = new SKColor(red, green, blue, effectiveAlpha);
+ maskImage.SetPixel(x, y, pixel);
+ }
+ else
+ {
+ var pixel = new SKColor(red, green, blue);
+ maskImage.SetPixel(x, y, pixel);
+ }
+ }
+ else
+ {
+ if (transparencyOptions == TransparencyOptions.CompareAlphaChannel)
+ {
+ var alpha = (byte)Math.Abs(actualPixel.Alpha - expectedPixel.Alpha);
+ var pixel = new SKColor(red, green, blue, alpha);
+ var r = Math.Abs(expectedPixel.Red - actualPixel.Red);
+ var g = Math.Abs(expectedPixel.Green - actualPixel.Green);
+ var b = Math.Abs(expectedPixel.Blue - actualPixel.Blue);
+ var a = Math.Abs(expectedPixel.Alpha - actualPixel.Alpha);
+ var sum = r + g + b + a;
+
+ if (sum > pixelColorShiftTolerance)
+ {
+ maskImage.SetPixel(x, y, pixel);
+ }
+ else
+ {
+ maskImage.SetPixel(x, y, 0);
+ }
+ }
+ else
+ {
+ var pixel = new SKColor(red, green, blue);
+ var r = Math.Abs(expectedPixel.Red - actualPixel.Red);
+ var g = Math.Abs(expectedPixel.Green - actualPixel.Green);
+ var b = Math.Abs(expectedPixel.Blue - actualPixel.Blue);
+ var sum = r + g + b;
+
+ if (sum > pixelColorShiftTolerance)
+ {
+ maskImage.SetPixel(x, y, pixel);
+ }
+ else
+ {
+ maskImage.SetPixel(x, y, 0);
+ }
+ }
+ }
+ }
+ }
+ return maskImage;
}
- using var actualImageCopy = new MemoryStream();
- using var expectedImageCopy = new MemoryStream();
- actualImage.CopyTo(actualImageCopy);
- expectedImage.CopyTo(expectedImageCopy);
- actualImageCopy.Position = 0;
- expectedImageCopy.Position = 0;
- using var actual = SKBitmap.Decode(actualImageCopy);
- using var expected = SKBitmap.Decode(expectedImageCopy);
- return CalcDiffMaskImage(actual, expected);
+ var grown = GrowToSameDimension(image1, image2);
+ try
+ {
+ return CalcDiffMaskImage(grown.Item1, grown.Item2, ResizeOption.DontResize, pixelColorShiftTolerance, transparencyOptions);
+ }
+ finally
+ {
+ grown.Item1?.Dispose();
+ grown.Item2?.Dispose();
+ }
}
///
/// Creates a diff mask image of two images
///
- ///
- ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
/// Image representing diff, black means no diff between actual image and expected image, white means max diff
- public static SKBitmap CalcDiffMaskImage(SKBitmap actual, SKBitmap expected)
+ public static SKBitmap CalcDiffMaskImage(SKBitmap image1, SKBitmap image2, SKBitmap mask, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0, TransparencyOptions transparencyOptions = TransparencyOptions.CompareAlphaChannel)
{
- if (!ImagesHaveSameDimension(actual, expected))
+ ArgumentNullException.ThrowIfNull(mask);
+
+ var imagesHaveSameDimension = ImagesHaveSameDimension(image1, image2) && ImagesHaveSameDimension(image1, mask);
+
+ if (resizeOption == ResizeOption.DontResize && !imagesHaveSameDimension)
{
throw new SkiaSharpCompareException(sizeDiffersExceptionMessage);
}
- var maskImage = new SKBitmap(actual.Width, actual.Height);
-
- for (var x = 0; x < actual.Width; x++)
+ if (imagesHaveSameDimension)
{
- for (var y = 0; y < actual.Height; y++)
- {
- var actualPixel = actual.GetPixel(x, y);
- var expectedPixel = expected.GetPixel(x, y);
-
- var red = (byte)Math.Abs(actualPixel.Red - expectedPixel.Red);
- var green = (byte)Math.Abs(actualPixel.Green - expectedPixel.Green);
- var blue = (byte)Math.Abs(actualPixel.Blue - expectedPixel.Blue);
- var pixel = new SKColor(red, green, blue);
+ var maskImage = new SKBitmap(image1.Width, image1.Height);
- maskImage.SetPixel(x, y, pixel);
+ for (var x = 0; x < image1.Width; x++)
+ {
+ for (var y = 0; y < image1.Height; y++)
+ {
+ var actualPixel = image1.GetPixel(x, y);
+ var expectedPixel = image2.GetPixel(x, y);
+ var maskPixel = mask.GetPixel(x, y);
+
+ var redDiff = Math.Abs(actualPixel.Red - expectedPixel.Red);
+ var greenDiff = Math.Abs(actualPixel.Green - expectedPixel.Green);
+ var blueDiff = Math.Abs(actualPixel.Blue - expectedPixel.Blue);
+
+ var red = (byte)Math.Max(0, redDiff - maskPixel.Red);
+ var green = (byte)Math.Max(0, greenDiff - maskPixel.Green);
+ var blue = (byte)Math.Max(0, blueDiff - maskPixel.Blue);
+
+ if (transparencyOptions == TransparencyOptions.CompareAlphaChannel)
+ {
+ var alphaDiff = Math.Abs(actualPixel.Alpha - expectedPixel.Alpha);
+ var alpha = (byte)Math.Max(0, alphaDiff - maskPixel.Alpha);
+ var pixel = new SKColor(red, green, blue, alpha);
+
+ if (pixelColorShiftTolerance == 0)
+ {
+ maskImage.SetPixel(x, y, pixel);
+ }
+ else
+ {
+ var r = Math.Abs(expectedPixel.Red - actualPixel.Red);
+ var g = Math.Abs(expectedPixel.Green - actualPixel.Green);
+ var b = Math.Abs(expectedPixel.Blue - actualPixel.Blue);
+ var a = Math.Abs(expectedPixel.Alpha - actualPixel.Alpha);
+ var sum = r + g + b + a;
+
+ if (sum > pixelColorShiftTolerance)
+ {
+ maskImage.SetPixel(x, y, pixel);
+ }
+ else
+ {
+ maskImage.SetPixel(x, y, 0);
+ }
+ }
+ }
+ else
+ {
+ var pixel = new SKColor(red, green, blue);
+
+ if (pixelColorShiftTolerance == 0)
+ {
+ maskImage.SetPixel(x, y, pixel);
+ }
+ else
+ {
+ var r = Math.Abs(expectedPixel.Red - actualPixel.Red);
+ var g = Math.Abs(expectedPixel.Green - actualPixel.Green);
+ var b = Math.Abs(expectedPixel.Blue - actualPixel.Blue);
+ var sum = r + g + b;
+
+ if (sum > pixelColorShiftTolerance)
+ {
+ maskImage.SetPixel(x, y, pixel);
+ }
+ else
+ {
+ maskImage.SetPixel(x, y, 0);
+ }
+ }
+ }
+ }
}
+ return maskImage;
}
- return maskImage;
+
+ var grown = GrowToSameDimension(image1, image2, mask);
+ try
+ {
+ return CalcDiffMaskImage(grown.Item1, grown.Item2, grown.Item3, ResizeOption.DontResize, pixelColorShiftTolerance, transparencyOptions);
+ }
+ finally
+ {
+ grown.Item1?.Dispose();
+ grown.Item2?.Dispose();
+ grown.Item3?.Dispose();
+ }
+ }
+
+ private static (SKBitmap, SKBitmap) GrowToSameDimension(SKBitmap image1, SKBitmap image2)
+ {
+ var biggestWidth = image1.Width > image2.Width ? image1.Width : image2.Width;
+ var biggestHeight = image1.Height > image2.Height ? image1.Height : image2.Height;
+ var skSize = new SKSizeI(biggestWidth, biggestHeight);
+ var grownExpected = image2.Resize(skSize, SKSamplingOptions.Default);
+ var grownActual = image1.Resize(skSize, SKSamplingOptions.Default);
+
+ return (grownActual, grownExpected);
+ }
+
+ private static (SKBitmap, SKBitmap, SKBitmap) GrowToSameDimension(SKBitmap image1, SKBitmap image2, SKBitmap mask)
+ {
+ var biggestWidth = image1.Width > image2.Width ? image1.Width : image2.Width;
+ biggestWidth = biggestWidth > mask.Width ? biggestWidth : mask.Width;
+ var biggestHeight = image1.Height > image2.Height ? image1.Height : image2.Height;
+ biggestHeight = biggestHeight > mask.Height ? biggestHeight : mask.Height;
+ var skSize = new SKSizeI(biggestWidth, biggestHeight);
+ var grownExpected = image2.Resize(skSize, SKSamplingOptions.Default);
+ var grownActual = image1.Resize(skSize, SKSamplingOptions.Default);
+ var grownMask = mask.Resize(skSize, SKSamplingOptions.Default);
+
+ return (grownActual, grownExpected, grownMask);
}
}
}
\ No newline at end of file
diff --git a/SkiaSharpCompare/SkiaSharpCompare.csproj b/SkiaSharpCompare/SkiaSharpCompare.csproj
index f4d7061..8b863a8 100644
--- a/SkiaSharpCompare/SkiaSharpCompare.csproj
+++ b/SkiaSharpCompare/SkiaSharpCompare.csproj
@@ -1,62 +1,60 @@
-
-
- netstandard2.0;net6.0
- true
- true
- https://github.com/Codeuctivity/SkiaSharp.Compare
- Compare Image JPG PNG BMP Linux Windows MacOs
- Stefan Seeland
- Codeuctivity
- $(CURRENT_VERSION)
- 0.0.1
- $(Version)
- $(Version)
- $(Version)-prerelease
- $(LAST_COMMIT_MESSAGE)
- NugetIcon.png
- https://github.com/Codeuctivity/SkiaSharp.Compare
- Compares Images and calculates mean error, absolute error and diff image. Supports optional tolerance mask/images to ignore areas of an image. Use this for automated visual comparing in your unit tests.
- Apache-2.0
- false
- SkiaSharpCompare.snk
- True
- true
- snupkg
- true
- true
- enable
- 8.0
- true
- Codeuctivity.SkiaSharpCompare
- nugetReadme.md
- Codeuctivity.SkiaSharpCompare
- Codeuctivity.SkiaSharpCompare
- Codeuctivity.SkiaSharpCompare
- true
-
-
- True
-
-
- True
-
+
+
+ net10.0
+ true
+ true
+ https://github.com/Codeuctivity/SkiaSharp.Compare
+ Compare Image JPG PNG BMP
+ Stefan Seeland
+ Codeuctivity
+ $(CURRENT_VERSION)
+ 0.0.1
+ $(Version)
+ $(Version)
+ $(Version)
+ $(LAST_COMMIT_MESSAGE)
+ NugetIcon.png
+ https://github.com/Codeuctivity/SkiaSharp.Compare
+
+ Compares Images and calculates mean error, absolute error and diff image.
+ Supports optional tolerance mask/images to ignore areas of an image. Use this for
+ automated visual comparing in your unit tests.
+
+ Apache-2.0
+ false
+ SkiaSharpCompare.snk
+ True
+ true
+ snupkg
+ true
+ true
+ enable
+ true
+ AllEnabledByDefault
+ latest
+ Codeuctivity.SkiaSharpCompare
+ nugetReadme.md
+ Codeuctivity.SkiaSharpCompare
+ Codeuctivity.SkiaSharpCompare
+ Codeuctivity.SkiaSharpCompare
+ true
+
+
+
+
-
-
-
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
+
+
+
-
-
-
-
-
\ No newline at end of file
+
diff --git a/SkiaSharpCompare/SkiaSharpCompareException.cs b/SkiaSharpCompare/SkiaSharpCompareException.cs
index d528cb2..d0198a6 100644
--- a/SkiaSharpCompare/SkiaSharpCompareException.cs
+++ b/SkiaSharpCompare/SkiaSharpCompareException.cs
@@ -1,12 +1,10 @@
using System;
-using System.Runtime.Serialization;
namespace Codeuctivity.SkiaSharpCompare
{
///
/// SkiaSharpCompareException gets thrown if comparing of images fails
///
- [Serializable]
public class SkiaSharpCompareException : Exception
{
///
@@ -34,15 +32,5 @@ public SkiaSharpCompareException(string message) : base(message)
public SkiaSharpCompareException(string message, Exception innerException) : base(message, innerException)
{
}
-
- ///
- /// SkiaSharpCompareException gets thrown if comparing of images fails
- ///
- ///
- ///
- ///
- protected SkiaSharpCompareException(SerializationInfo info, StreamingContext context) : base(info, context)
- {
- }
}
}
\ No newline at end of file
diff --git a/SkiaSharpCompare/TransparencyOptions.cs b/SkiaSharpCompare/TransparencyOptions.cs
new file mode 100644
index 0000000..3da27dd
--- /dev/null
+++ b/SkiaSharpCompare/TransparencyOptions.cs
@@ -0,0 +1,22 @@
+namespace Codeuctivity.SkiaSharpCompare
+{
+ ///
+ /// Specifies options for handling transparency when comparing images.
+ ///
+ /// These options determine whether the alpha channel (transparency) is considered during image
+ /// comparison.
+ public enum TransparencyOptions
+ {
+ ///
+ /// Gets or sets a value indicating whether the alpha channel should be ignored during processing.
+ ///
+ IgnoreAlphaChannel,
+
+ ///
+ /// Compares the alpha channel values of two colors.
+ ///
+ /// This method is useful for sorting or comparing colors based on their transparency
+ /// levels.
+ CompareAlphaChannel
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompare/docs/nugetReadme.md b/SkiaSharpCompare/docs/nugetReadme.md
index d16f7a9..96f410b 100644
--- a/SkiaSharpCompare/docs/nugetReadme.md
+++ b/SkiaSharpCompare/docs/nugetReadme.md
@@ -1,13 +1,47 @@
+# Compares images
-Compares images
-Supports comparing images by using a tolerance mask image.
-Linux, Windows, MacOs supported.
-
-Basic example:
+## Compares each pixel to determine the equality
```csharp
-bool isEqual = SkiaSharpCompare.ImagesAreEqual("actual.png", "expected.png");
+bool isEqual = Compare.ImagesAreEqual("actual.png", "expected.png");
+```
-// calcs MeanError, AbsoluteError, PixelErrorCount and PixelErrorPercentage
-ICompareResult calcDiff = SkiaSharpCompare.CalcDiff("actual.png", "expected.png");
+## [Calculates diff](https://dotnetfiddle.net/tTnq2j)
+
+```csharp
+var calcDiff = Compare.CalcDiff("2x2PixelBlack.png", "2x2PixelWhite.png");
+Console.WriteLine($"PixelErrorCount: {calcDiff.PixelErrorCount}");
+Console.WriteLine($"PixelErrorPercentage: {calcDiff.PixelErrorPercentage}");
+Console.WriteLine($"AbsoluteError: {calcDiff.AbsoluteError}");
+Console.WriteLine($"MeanError: {calcDiff.MeanError}");
+// PixelErrorCount: 4
+// PixelErrorPercentage: 100
+// AbsoluteError: 3060
+// MeanError: 765
```
+
+## [Configure transparency, enable metadata compare, ...](https://dotnetfiddle.net/lygaRU)
+
+```csharp
+var comparer = new ImageCompare(ResizeOption.Resize, TransparencyOptions.CompareAlphaChannel, pixelColorShiftTolerance: 5, compareMetadata: true);
+var calcDiff = comparer.CalcDiff("pngPartialTransparent4x4Pixel.png", "2x2PixelWhite.png");
+// Displaying the differences
+Console.WriteLine($"PixelErrorCount: {calcDiff.PixelErrorCount}");
+Console.WriteLine($"PixelErrorPercentage: {calcDiff.PixelErrorPercentage}");
+Console.WriteLine($"AbsoluteError: {calcDiff.AbsoluteError}");
+Console.WriteLine($"MeanError: {calcDiff.MeanError}");
+foreach (var metadataDifference in calcDiff.MetadataDifferences)
+ Console.WriteLine($"Metadata Difference: {metadataDifference}");
+
+// PixelErrorCount: 16
+// PixelErrorPercentage: 100
+// AbsoluteError: 14272
+// MeanError: 892
+// Metadata Difference: [File:File Name, (pngPartialTransparent4x4Pixel.png, 2x2PixelWhite.png)]
+// Metadata Difference: [File:File Size, (688 bytes, 556 bytes)]
+// Metadata Difference: [PNG-IHDR:Color Type, (True Color with Alpha, True Color)]
+// Metadata Difference: [PNG-IHDR:Image Height, (4, 2)]
+// Metadata Difference: [PNG-IHDR:Image Width, (4, 2)]
+// Metadata Difference: [PNG-tEXt:Textual Data, (Software: Paint.NET 5.1.2, Comment: Created with GIMP)]
+// ...
+```
\ No newline at end of file
diff --git a/SkiaSharpCompareTestNunit/ImageExtensions.cs b/SkiaSharpCompareTestNunit/ImageExtensions.cs
new file mode 100644
index 0000000..cbc9050
--- /dev/null
+++ b/SkiaSharpCompareTestNunit/ImageExtensions.cs
@@ -0,0 +1,35 @@
+using SkiaSharp;
+using System.IO;
+
+namespace SkiaSharpCompareTestNunit
+{
+ internal class ImageExtensions
+ {
+ protected ImageExtensions()
+ {
+ }
+
+ internal static void SaveAsPng(SKBitmap diffMask1Image, FileStream diffMask1Stream)
+ {
+ var encodedData = diffMask1Image.Encode(SKEncodedImageFormat.Png, 100);
+ encodedData.SaveTo(diffMask1Stream);
+ }
+
+ public static bool IsImageEntirelyBlack(SKBitmap image, Codeuctivity.SkiaSharpCompare.TransparencyOptions transparencyOptions)
+ {
+ for (var x = 0; x < image.Width; x++)
+ {
+ for (var y = 0; y < image.Height; y++)
+ {
+ var sKColor = image.GetPixel(x, y);
+ if (sKColor.Red != 0 || sKColor.Green != 0 || sKColor.Blue != 0 || (transparencyOptions == Codeuctivity.SkiaSharpCompare.TransparencyOptions.CompareAlphaChannel && sKColor.Alpha != 0))
+ {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompareTestNunit/ImageSharpCompareTest.cs b/SkiaSharpCompareTestNunit/ImageSharpCompareTest.cs
deleted file mode 100644
index e944a08..0000000
--- a/SkiaSharpCompareTestNunit/ImageSharpCompareTest.cs
+++ /dev/null
@@ -1,279 +0,0 @@
-using Codeuctivity.SkiaSharpCompare;
-using NUnit.Framework;
-using SkiaSharp;
-using System;
-using System.IO;
-using System.Reflection;
-
-namespace SkiaSharpCompareTestNunit
-{
- public class IntegrationTest
- {
- private const string jpg0Rgb24 = "../../../TestData/Calc0.jpg";
- private const string jpg1Rgb24 = "../../../TestData/Calc1.jpg";
- private const string png0Rgba32 = "../../../TestData/Calc0.png";
- private const string png1Rgba32 = "../../../TestData/Calc1.png";
- private const string pngBlack = "../../../TestData/Black.png";
- private const string pngWhite = "../../../TestData/White.png";
-
- [Test]
- [TestCase(jpg0Rgb24, jpg0Rgb24, true)]
- [TestCase(png0Rgba32, png0Rgba32, true)]
- [TestCase(png0Rgba32, jpg0Rgb24, true)]
- [TestCase(png0Rgba32, jpg1Rgb24, true)]
- [TestCase(png0Rgba32, pngBlack, false)]
- public void ShouldVerifyThatImagesFromFilepathSizeAreEqual(string pathActual, string pathExpected, bool expectedOutcome)
- {
- var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
- var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
-
- Assert.That(SkiaSharpCompare.ImagesHaveEqualSize(absolutePathActual, absolutePathExpected), Is.EqualTo(expectedOutcome));
- }
-
- [Test]
- [TestCase(jpg0Rgb24, jpg0Rgb24, true)]
- [TestCase(png0Rgba32, png0Rgba32, true)]
- [TestCase(png0Rgba32, jpg0Rgb24, true)]
- [TestCase(png0Rgba32, jpg1Rgb24, true)]
- [TestCase(png0Rgba32, pngBlack, false)]
- public void ShouldVerifyThatImagesSizeAreEqual(string pathActual, string pathExpected, bool expectedOutcome)
- {
- var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
- var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
-
- using var actual = SKBitmap.Decode(absolutePathActual);
- using var expected = SKBitmap.Decode(absolutePathExpected);
-
- Assert.That(SkiaSharpCompare.ImagesHaveEqualSize(absolutePathActual, absolutePathExpected), Is.EqualTo(expectedOutcome));
- }
-
- [Test]
- [TestCase(jpg0Rgb24, jpg0Rgb24, true)]
- [TestCase(png0Rgba32, png0Rgba32, true)]
- [TestCase(png0Rgba32, jpg0Rgb24, true)]
- [TestCase(png0Rgba32, jpg1Rgb24, true)]
- [TestCase(png0Rgba32, pngBlack, false)]
- public void ShouldVerifyThatImageStreamsSizeAreEqual(string pathActual, string pathExpected, bool expectedOutcome)
- {
- var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
- var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
-
- using var actual = new FileStream(absolutePathActual, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- using var expected = new FileStream(absolutePathExpected, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
-
- Assert.That(SkiaSharpCompare.ImagesHaveEqualSize(absolutePathActual, absolutePathExpected), Is.EqualTo(expectedOutcome));
- }
-
- [Test]
- [TestCase(jpg0Rgb24, jpg0Rgb24)]
- [TestCase(png0Rgba32, png0Rgba32)]
- public void ShouldVerifyThatImagesAreEqual(string pathActual, string pathExpected)
- {
- var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
- var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
-
- Assert.That(SkiaSharpCompare.ImagesAreEqual(absolutePathActual, absolutePathExpected), Is.True);
- }
-
- [Test]
- [TestCase(jpg0Rgb24, jpg0Rgb24)]
- [TestCase(png0Rgba32, png0Rgba32)]
- public void ShouldVerifyThatImageStreamsAreEqual(string pathActual, string pathExpected)
- {
- var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
- var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
-
- using var actual = new FileStream(absolutePathActual, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- using var expected = new FileStream(absolutePathExpected, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
-
- Assert.That(SkiaSharpCompare.ImagesAreEqual(actual, expected), Is.True);
- }
-
- [Test]
- [TestCase(jpg0Rgb24, jpg0Rgb24)]
- [TestCase(png0Rgba32, png0Rgba32)]
- public void ShouldVerifyThatImageSharpImagesAreEqual(string pathActual, string pathExpected)
- {
- var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
- var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
-
- using var actual = SKBitmap.Decode(absolutePathActual);
- using var expected = SKBitmap.Decode(absolutePathExpected);
-
- Assert.That(SkiaSharpCompare.ImagesAreEqual(actual, expected), Is.True);
- }
-
- [Test]
- [TestCase(jpg0Rgb24, png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d)]
- [TestCase(jpg1Rgb24, png1Rgba32, 460034, 2.8459701566405187d, 158121, 97.820519165573728d)]
- [TestCase(png1Rgba32, png1Rgba32, 0, 0, 0, 0)]
- [TestCase(jpg1Rgb24, jpg1Rgb24, 0, 0, 0, 0)]
- [TestCase(jpg0Rgb24, jpg1Rgb24, 208890, 1.2922842790329365d, 2101, 1.2997698646408156d)]
- [TestCase(png0Rgba32, png1Rgba32, 203027, 1.25601321422385d, 681, 0.42129618173269651d)]
- [TestCase(pngBlack, pngWhite, 3060, 765, 4, 100.0d)]
- public void ShouldVerifyThatImagesAreSemiEqual(string pathPic1, string pathPic2, int expectedAbsoluteError, double expectedMeanError, int expectedPixelErrorCount, double expectedPixelErrorPercentage)
- {
- var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
- var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
-
- var diff = SkiaSharpCompare.CalcDiff(absolutePathPic1, absolutePathPic2);
- Assert.That(diff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError");
- Assert.That(diff.MeanError, Is.EqualTo(expectedMeanError), "MeanError");
- Assert.That(diff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount");
- Assert.That(diff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage");
- }
-
- [Test]
- [TestCase(jpg0Rgb24, png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d)]
- [TestCase(jpg1Rgb24, png1Rgba32, 460034, 2.8459701566405187d, 158121, 97.820519165573728d)]
- [TestCase(png1Rgba32, png1Rgba32, 0, 0, 0, 0)]
- [TestCase(jpg1Rgb24, jpg1Rgb24, 0, 0, 0, 0)]
- [TestCase(jpg0Rgb24, jpg1Rgb24, 208890, 1.2922842790329365d, 2101, 1.2997698646408156d)]
- [TestCase(png0Rgba32, png1Rgba32, 203027, 1.25601321422385d, 681, 0.42129618173269651d)]
- [TestCase(pngBlack, pngWhite, 3060, 765, 4, 100.0d)]
- public void ShouldVerifyThatImageStreamsAreSemiEqual(string pathPic1, string pathPic2, int expectedAbsoluteError, double expectedMeanError, int expectedPixelErrorCount, double expectedPixelErrorPercentage)
- {
- var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
- var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
-
- using var pic1 = new FileStream(absolutePathPic1, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- using var pic2 = new FileStream(absolutePathPic2, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
-
- var diff = SkiaSharpCompare.CalcDiff(pic1, pic2);
- Assert.That(diff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError");
- Assert.That(diff.MeanError, Is.EqualTo(expectedMeanError), "MeanError");
- Assert.That(diff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount");
- Assert.That(diff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage");
- }
-
- [TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0)]
- public void ShoulCalcDiffmask(string pathPic1, string pathPic2, int expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage)
- {
- 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 = SkiaSharpCompare.CalcDiffMaskImage(absolutePathPic1, absolutePathPic2))
- {
- SaveAsPng(maskImage, fileStreamDifferenceMask);
- }
-
- var maskedDiff = SkiaSharpCompare.CalcDiff(absolutePathPic1, absolutePathPic2, differenceMask);
- File.Delete(differenceMask);
-
- 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");
- }
-
- private void SaveAsPng(SKBitmap maskImage, FileStream fileStreamDifferenceMask)
- {
- var encodedData = maskImage.Encode(SKEncodedImageFormat.Png, 100);
- encodedData.SaveTo(fileStreamDifferenceMask);
- }
-
- [TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0)]
- [TestCase(jpg0Rgb24, jpg1Rgb24, 0, 0, 0, 0)]
- public void ShoulCalcDiffmaskImageSharp(string pathPic1, string pathPic2, int expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage)
- {
- var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
- var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
- var differenceMaskPicPath = Path.GetTempFileName() + "differenceMask.png";
-
- using var absolutePic1 = SKBitmap.Decode(absolutePathPic1);
- using var absolutePic2 = SKBitmap.Decode(absolutePathPic2);
-
- using (var fileStreamDifferenceMask = File.Create(differenceMaskPicPath))
- using (var maskImage = SkiaSharpCompare.CalcDiffMaskImage(absolutePic1, absolutePic2))
- {
- SaveAsPng(maskImage, fileStreamDifferenceMask);
- }
-
- using var differenceMaskPic = SKBitmap.Decode(differenceMaskPicPath);
- var maskedDiff = SkiaSharpCompare.CalcDiff(absolutePic1, absolutePic2, differenceMaskPic);
- 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");
- }
-
- private static object? GetInstanceField(T instance, string fieldName)
- {
- var bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
- var field = typeof(T).GetField(fieldName, bindFlags);
- if (field == null)
- {
- throw new ArgumentNullException(fieldName);
- }
-
- return 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)
- {
- var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
- var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
-
- using var pic1 = new FileStream(absolutePathPic1, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- using var pic2 = new FileStream(absolutePathPic2, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
-
- using var maskImage = SkiaSharpCompare.CalcDiffMaskImage(pic1, pic2);
-
- pic1.Position = 0;
- pic2.Position = 0;
-
- var maskedDiff = SkiaSharpCompare.CalcDiff(pic1, pic2, maskImage);
- 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");
- }
-
- [Test]
- [TestCase(jpg0Rgb24, jpg1Rgb24)]
- [TestCase(png0Rgba32, png1Rgba32)]
- [TestCase(jpg0Rgb24, png1Rgba32)]
- [TestCase(jpg0Rgb24, png0Rgba32)]
- [TestCase(jpg1Rgb24, png1Rgba32)]
- public void ShouldVerifyThatImagesAreNotEqual(string pathActual, string pathExpected)
- {
- var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
- var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
-
- Assert.That(SkiaSharpCompare.ImagesAreEqual(absolutePathActual, absolutePathExpected), Is.False);
- }
-
- [Test]
- [TestCase(jpg0Rgb24, jpg1Rgb24)]
- [TestCase(png0Rgba32, png1Rgba32)]
- [TestCase(jpg0Rgb24, png1Rgba32)]
- [TestCase(jpg0Rgb24, png0Rgba32)]
- [TestCase(jpg1Rgb24, png1Rgba32)]
- public void ShouldVerifyThatImageStreamAreNotEqual(string pathActual, string pathExpected)
- {
- var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
- var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
-
- using var actual = new FileStream(absolutePathActual, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
- using var expected = new FileStream(absolutePathExpected, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
-
- Assert.That(SkiaSharpCompare.ImagesAreEqual(actual, expected), Is.False);
- }
-
- [TestCase(png0Rgba32, pngBlack)]
- public void ShouldVerifyThatImageWithDifferentSizeThrows(string pathPic1, string pathPic2)
- {
- var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
- var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
-
- var exception = Assert.Throws(() => SkiaSharpCompare.CalcDiff(absolutePathPic1, absolutePathPic2));
-
- Assert.That(exception?.Message, Is.EqualTo("Size of images differ."));
- }
- }
-}
\ No newline at end of file
diff --git a/SkiaSharpCompareTestNunit/SkiaSharpCompareMetaData_PathTests.cs b/SkiaSharpCompareTestNunit/SkiaSharpCompareMetaData_PathTests.cs
new file mode 100644
index 0000000..ffeb658
--- /dev/null
+++ b/SkiaSharpCompareTestNunit/SkiaSharpCompareMetaData_PathTests.cs
@@ -0,0 +1,112 @@
+using Codeuctivity.SkiaSharpCompare;
+using NUnit.Framework;
+using SkiaSharp;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace SkiaSharpCompareTestNunit
+{
+ internal class SkiaSharpCompareMetaData_PathTests
+ {
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, true, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, false, true)]
+ [TestCase(TestFiles.imageWithoutGpsMetadata, TestFiles.imageWithGpsMetadata, true, false)]
+ [TestCase(TestFiles.imageWithoutGpsMetadata, TestFiles.imageWithGpsMetadata, false, true)]
+ public void ImagesAreEqual_SamePixelComparedByMetadataShouldReturnResult(string pic1Path, string pic2Path, bool shouldCompareMetadata, bool expectOutcomeComparisonOfImage)
+ {
+ var pic1 = Path.Combine(AppContext.BaseDirectory, pic1Path);
+ var pic2 = Path.Combine(AppContext.BaseDirectory, pic2Path);
+
+ var sut = new ImageCompare(compareMetadata: shouldCompareMetadata);
+ Assert.That(sut.ImagesAreEqual(pic1, pic2), Is.EqualTo(expectOutcomeComparisonOfImage));
+ }
+
+ [Test]
+ public void CalcDiff_SamePixelComparedByMetadataShouldReturnResult_Null()
+ {
+ var pic1 = Path.Combine(AppContext.BaseDirectory, TestFiles.imageWithGpsMetadata);
+ var pic2 = Path.Combine(AppContext.BaseDirectory, TestFiles.imageWithoutGpsMetadata);
+
+ var sut = new ImageCompare(compareMetadata: false);
+ var actual = sut.CalcDiff(pic1, pic2);
+
+ Assert.That(actual.MetadataDifferences, Is.Null);
+ Assert.That(actual.PixelErrorCount, Is.Zero);
+ }
+
+ [Test]
+ public void CalcDiff_SamePixelComparedByMetadataShouldReturnEmptyResult()
+ {
+ var pic1 = Path.Combine(AppContext.BaseDirectory, TestFiles.imageWithoutGpsMetadata);
+ var pic2 = Path.Combine(AppContext.BaseDirectory, TestFiles.imageWithoutGpsMetadata);
+
+ var sut = new ImageCompare(compareMetadata: true);
+ var actual = sut.CalcDiff(pic1, pic2);
+
+ Assert.That(actual.MetadataDifferences, Is.Empty);
+ Assert.That(actual.PixelErrorCount, Is.Zero);
+ }
+
+ [Test]
+ [SetCulture("en-US")]
+ public void CalcDiff_SamePixelComparedByMetadataShouldReturnCollectionOfMetadataThatDiffers()
+ {
+ var pic1 = Path.Combine(AppContext.BaseDirectory, TestFiles.imageWithoutGpsMetadata);
+ var pic2 = Path.Combine(AppContext.BaseDirectory, TestFiles.imageWithGpsMetadata);
+
+ var sut = new ImageCompare(compareMetadata: true);
+ var actual = sut.CalcDiff(pic1, pic2);
+
+ // Output actual and expected to test run logs for easier diagnosis
+ TestContext.WriteLine($"Actual image: {pic1}");
+ TestContext.WriteLine($"Expected image: {pic2}");
+ TestContext.WriteLine($"Actual MetadataDifferences: {FormatMetadata(actual.MetadataDifferences)}");
+
+ var expected = new Dictionary
+ {
+ { "File:File Name", (Path.GetFileName(pic1), Path.GetFileName(pic2)) },
+ { "GPS:GPS Altitude", ("0 metres", "201.62 metres") },
+ { "GPS:GPS Date Stamp", ("", "2025:01:03") },
+ { "GPS:GPS Latitude", ("0° 0' 0\"", "48° 12' 7.17\"") },
+ { "GPS:GPS Latitude Ref", ("", "N") },
+ { "GPS:GPS Longitude", ("0° 0' 0\"", "16° 24' 7.53\"") },
+ { "GPS:GPS Longitude Ref", ("", "E") },
+ { "GPS:GPS Processing Method", ("", "GPS") },
+ { "GPS:GPS Time-Stamp", ("00:00:00.000 UTC", "14:41:20.000 UTC") }
+ };
+
+ TestContext.WriteLine($"Expected MetadataDifferences: {FormatMetadata(expected)}");
+
+ Assert.That(actual.MetadataDifferences, Is.EqualTo(expected));
+ Assert.That(actual.PixelErrorCount, Is.Zero);
+ }
+
+ [Test]
+ public void CreateToleranceMaskFromDiff_CreatesWhiteRectangle()
+ {
+ var pic1 = Path.Combine(AppContext.BaseDirectory, TestFiles.png0Rgba32);
+ var pic2 = Path.Combine(AppContext.BaseDirectory, TestFiles.png1Rgba32);
+ var sut = new ImageCompare();
+
+ using var diffMask = sut.CalcDiffMaskImage(pic1, pic2);
+ using var tolFromBitmap = sut.CreateToleranceMaskFromDiff(diffMask);
+
+ var expectedImage = SKBitmap.Decode(TestFiles.createToleranceMaskFromDiff);
+
+ Assert.That(sut.ImagesAreEqual(tolFromBitmap, expectedImage), Is.True);
+ Assert.That(sut.CalcDiff(pic1, pic1, TestFiles.createToleranceMaskFromDiff).PixelErrorCount, Is.Zero);
+ }
+
+ private static string FormatMetadata(Dictionary? metadata)
+ {
+ if (metadata is null)
+ {
+ return "null";
+ }
+
+ return string.Join("; ", metadata.Select(kvp => $"{kvp.Key}=[{kvp.Value.ValueA ?? ""} | {kvp.Value.ValueB ?? ""}]"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompareTestNunit/SkiaSharpCompareMetaData_SkBitmap.cs b/SkiaSharpCompareTestNunit/SkiaSharpCompareMetaData_SkBitmap.cs
new file mode 100644
index 0000000..920f66e
--- /dev/null
+++ b/SkiaSharpCompareTestNunit/SkiaSharpCompareMetaData_SkBitmap.cs
@@ -0,0 +1,78 @@
+using Codeuctivity.SkiaSharpCompare;
+using NUnit.Framework;
+using SkiaSharp;
+using System;
+using System.IO;
+
+namespace SkiaSharpCompareTestNunit
+{
+ internal class SkiaSharpCompareMetaData_SkBitmap
+ {
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32)]
+ [TestCase(TestFiles.imageWithoutGpsMetadata, TestFiles.imageWithGpsMetadata)]
+ public void ImagesAreEqual_SamePixelComparedByMetadataShouldReturnResult(string pic1Path, string pic2Path)
+ {
+ var pic1 = SKBitmap.Decode(Path.Combine(AppContext.BaseDirectory, pic1Path));
+ var pic2 = SKBitmap.Decode(Path.Combine(AppContext.BaseDirectory, pic2Path));
+
+ var sut = new ImageCompare(compareMetadata: false);
+ Assert.That(sut.ImagesAreEqual(pic1, pic2));
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32)]
+ [TestCase(TestFiles.imageWithoutGpsMetadata, TestFiles.imageWithGpsMetadata)]
+ public void ImagesAreEqual_SamePixelComparedByMetadataShouldThrows(string pic1Path, string pic2Path)
+ {
+ var pic1 = SKBitmap.Decode(Path.Combine(AppContext.BaseDirectory, pic1Path));
+ var pic2 = SKBitmap.Decode(Path.Combine(AppContext.BaseDirectory, pic2Path));
+ var sut = new ImageCompare(compareMetadata: true);
+ Action act = () => sut.ImagesAreEqual(pic1, pic2);
+
+ var ex = Assert.Throws(act);
+
+ Assert.That(ex?.Message, Is.EqualTo("Metadata comparison is not implemented for SKBitmap inputs. https://github.com/mono/SkiaSharp/issues/1139 Use the overload with streams or filepath to get support for metadata comparison."));
+ }
+
+ [Test]
+ public void CalcDiff_SamePixelComparedByMetadataShouldReturnResult_Null()
+ {
+ var pic1 = SKBitmap.Decode(Path.Combine(AppContext.BaseDirectory, TestFiles.imageWithGpsMetadata));
+ var pic2 = SKBitmap.Decode(Path.Combine(AppContext.BaseDirectory, TestFiles.imageWithoutGpsMetadata));
+
+ var sut = new ImageCompare(compareMetadata: false);
+ var actual = sut.CalcDiff(pic1, pic2);
+
+ Assert.That(actual.MetadataDifferences, Is.Null);
+ Assert.That(actual.PixelErrorCount, Is.Zero);
+ }
+
+ [Test]
+ public void CalcDiff_SamePixelComparedByMetadataShouldReturnEmptyResult()
+ {
+ var pic1 = SKBitmap.Decode(Path.Combine(AppContext.BaseDirectory, TestFiles.imageWithoutGpsMetadata));
+ var pic2 = SKBitmap.Decode(Path.Combine(AppContext.BaseDirectory, TestFiles.imageWithoutGpsMetadata));
+
+ var sut = new ImageCompare(compareMetadata: true);
+ Action act = () => sut.ImagesAreEqual(pic1, pic2);
+
+ var ex = Assert.Throws(act);
+
+ Assert.That(ex?.Message, Is.EqualTo("Metadata comparison is not implemented for SKBitmap inputs. https://github.com/mono/SkiaSharp/issues/1139 Use the overload with streams or filepath to get support for metadata comparison."));
+ }
+
+ [Test]
+ [SetCulture("en-US")]
+ public void CalcDiff_SamePixelComparedByMetadataShouldReturnCollectionOfMetadataThatDiffers()
+ {
+ var pic1 = SKBitmap.Decode(Path.Combine(AppContext.BaseDirectory, TestFiles.imageWithoutGpsMetadata));
+ var pic2 = SKBitmap.Decode(Path.Combine(AppContext.BaseDirectory, TestFiles.imageWithGpsMetadata));
+
+ var sut = new ImageCompare(compareMetadata: true);
+ Action act = () => sut.ImagesAreEqual(pic1, pic2);
+
+ var ex = Assert.Throws(act);
+
+ Assert.That(ex?.Message, Is.EqualTo("Metadata comparison is not implemented for SKBitmap inputs. https://github.com/mono/SkiaSharp/issues/1139 Use the overload with streams or filepath to get support for metadata comparison."));
+ }
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompareTestNunit/SkiaSharpCompareMetaData_StreamTests.cs b/SkiaSharpCompareTestNunit/SkiaSharpCompareMetaData_StreamTests.cs
new file mode 100644
index 0000000..5490fc2
--- /dev/null
+++ b/SkiaSharpCompareTestNunit/SkiaSharpCompareMetaData_StreamTests.cs
@@ -0,0 +1,92 @@
+using Codeuctivity.SkiaSharpCompare;
+using NUnit.Framework;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace SkiaSharpCompareTestNunit
+{
+ internal class SkiaSharpCompareMetaData_StreamTests
+ {
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, true, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, false, true)]
+ [TestCase(TestFiles.imageWithoutGpsMetadata, TestFiles.imageWithGpsMetadata, true, false)]
+ [TestCase(TestFiles.imageWithoutGpsMetadata, TestFiles.imageWithGpsMetadata, false, true)]
+ public void ImagesAreEqual_SamePixelComparedByMetadataShouldReturnResult(string pic1Path, string pic2Path, bool shouldCompareMetadata, bool expectOutcomeComparisonOfImage)
+ {
+ using var pic1 = File.OpenRead(pic1Path);
+ using var pic2 = File.OpenRead(pic2Path);
+
+ var sut = new ImageCompare(compareMetadata: shouldCompareMetadata);
+ Assert.That(sut.ImagesAreEqual(pic1, pic2), Is.EqualTo(expectOutcomeComparisonOfImage));
+ }
+
+ [Test]
+ public void CalcDiff_SamePixelComparedByMetadataShouldReturnResult_Null()
+ {
+ using var pic1 = File.OpenRead(TestFiles.imageWithoutGpsMetadata);
+ using var pic2 = File.OpenRead(TestFiles.imageWithoutGpsMetadata);
+
+ var sut = new ImageCompare(compareMetadata: false);
+ var actual = sut.CalcDiff(pic1, pic2);
+
+ Assert.That(actual.MetadataDifferences, Is.Null);
+ Assert.That(actual.PixelErrorCount, Is.Zero);
+ }
+
+ [Test]
+ public void CalcDiff_SamePixelComparedByMetadataShouldReturnEmptyResult()
+ {
+ using var pic1 = File.OpenRead(TestFiles.imageWithoutGpsMetadata);
+ using var pic2 = File.OpenRead(TestFiles.imageWithoutGpsMetadata);
+
+ var sut = new ImageCompare(compareMetadata: true);
+ var actual = sut.CalcDiff(pic1, pic2);
+
+ Assert.That(actual.MetadataDifferences, Is.Empty);
+ Assert.That(actual.PixelErrorCount, Is.Zero);
+ }
+
+ [Test]
+ [SetCulture("en-US")]
+ public void CalcDiff_SamePixelComparedByMetadataShouldReturnCollectionOfMetadataThatDiffers()
+ {
+ using var pic1 = File.OpenRead(TestFiles.imageWithoutGpsMetadata);
+ using var pic2 = File.OpenRead(TestFiles.imageWithGpsMetadata);
+ var sut = new ImageCompare(compareMetadata: true);
+ var actual = sut.CalcDiff(pic1, pic2);
+
+ // Output actual and expected to test run logs for easier diagnosis
+ TestContext.WriteLine($"Actual image: {pic1}");
+ TestContext.WriteLine($"Expected image: {pic2}");
+ TestContext.WriteLine($"Actual MetadataDifferences: {FormatMetadata(actual.MetadataDifferences)}");
+
+ var expected = new Dictionary
+ {
+ { "GPS:GPS Altitude", ("0 metres", "201.62 metres") },
+ { "GPS:GPS Date Stamp", ("", "2025:01:03") },
+ { "GPS:GPS Latitude", ("0° 0' 0\"", "48° 12' 7.17\"") },
+ { "GPS:GPS Latitude Ref", ("", "N") },
+ { "GPS:GPS Longitude", ("0° 0' 0\"", "16° 24' 7.53\"") },
+ { "GPS:GPS Longitude Ref", ("", "E") },
+ { "GPS:GPS Processing Method", ("", "GPS") },
+ { "GPS:GPS Time-Stamp", ("00:00:00.000 UTC", "14:41:20.000 UTC") }
+ };
+
+ TestContext.WriteLine($"Expected MetadataDifferences: {FormatMetadata(expected)}");
+
+ Assert.That(actual.MetadataDifferences, Is.EqualTo(expected));
+ Assert.That(actual.PixelErrorCount, Is.Zero);
+ }
+
+ private static string FormatMetadata(Dictionary? metadata)
+ {
+ if (metadata is null)
+ {
+ return "null";
+ }
+
+ return string.Join("; ", metadata.Select(kvp => $"{kvp.Key}=[{kvp.Value.ValueA ?? ""} | {kvp.Value.ValueB ?? ""}]"));
+ }
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompareTestNunit/SkiaSharpCompareTestNunit.csproj b/SkiaSharpCompareTestNunit/SkiaSharpCompareTestNunit.csproj
index 50929f4..dad41e9 100644
--- a/SkiaSharpCompareTestNunit/SkiaSharpCompareTestNunit.csproj
+++ b/SkiaSharpCompareTestNunit/SkiaSharpCompareTestNunit.csproj
@@ -1,37 +1,31 @@
-
- net48;net6.0
- net6.0
- false
- 8.0
- enable
- true
-
+
+ net10.0
+ false
+ enable
+ true
+
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
- 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
+
+
+
-
-
-
-
-
-
-
+
+
+
diff --git a/SkiaSharpCompareTestNunit/SkiaSharpCompareTests.cs b/SkiaSharpCompareTestNunit/SkiaSharpCompareTests.cs
new file mode 100644
index 0000000..1c5c1ef
--- /dev/null
+++ b/SkiaSharpCompareTestNunit/SkiaSharpCompareTests.cs
@@ -0,0 +1,507 @@
+using Codeuctivity.SkiaSharpCompare;
+using NUnit.Framework;
+using SkiaSharp;
+using System;
+using System.IO;
+
+namespace SkiaSharpCompareTestNunit
+{
+ public class SkiaSharpCompareTests
+ {
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg0Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.jpg0Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.jpg1Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.pngBlack2x2px, false)]
+ public void ShouldVerifyThatImagesFromFilePathSizeAreEqual(string pathActual, string pathExpected, bool expectedOutcome)
+ {
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ Assert.That(ImageCompare.ImagesHaveEqualSize(absolutePathActual, absolutePathExpected), Is.EqualTo(expectedOutcome));
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg0Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.jpg0Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.jpg1Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.pngBlack2x2px, false)]
+ public void ShouldVerifyThatImagesSizeAreEqual(string pathActual, string pathExpected, bool expectedOutcome)
+ {
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ using var actual = SKBitmap.Decode(absolutePathActual);
+ using var expected = SKBitmap.Decode(absolutePathExpected);
+
+ Assert.That(ImageCompare.ImagesHaveEqualSize(absolutePathActual, absolutePathExpected), Is.EqualTo(expectedOutcome));
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg0Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.jpg0Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.jpg1Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.pngBlack2x2px, false)]
+ public void ShouldVerifyThatImageStreamsSizeAreEqual(string pathActual, string pathExpected, bool expectedOutcome)
+ {
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ using var actual = new FileStream(absolutePathActual, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var expected = new FileStream(absolutePathExpected, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ Assert.That(ImageCompare.ImagesHaveEqualSize(absolutePathActual, absolutePathExpected), Is.EqualTo(expectedOutcome));
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg0Rgb24, ResizeOption.Resize, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, ResizeOption.Resize, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg0Rgb24, ResizeOption.DontResize, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, ResizeOption.DontResize, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.pngTransparent4x4px, TestFiles.pngPartialTransparent4x4px, ResizeOption.DontResize, TransparencyOptions.IgnoreAlphaChannel)]
+ public void ShouldVerifyThatImagesAreEqual(string pathActual, string pathExpected, ResizeOption resizeOption, TransparencyOptions transparencyOptions)
+ {
+ var sut = new ImageCompare(resizeOption, transparencyOptions);
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ Assert.That(sut.ImagesAreEqual(absolutePathActual, absolutePathExpected), Is.True);
+ }
+
+ [Test]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px, ResizeOption.Resize, true)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, ResizeOption.Resize, true)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, ResizeOption.DontResize, false)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, ResizeOption.DontResize, false)]
+ public void ShouldVerifyThatImagesWithDifferentSizeAreEqual(string pathActual, string pathExpected, ResizeOption resizeOption, bool expectedResult)
+ {
+ var sut = new ImageCompare(resizeOption);
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ Assert.That(sut.ImagesAreEqual(absolutePathActual, absolutePathExpected), Is.EqualTo(expectedResult));
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg0Rgb24)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32)]
+ public void ShouldVerifyThatImageStreamsAreEqual(string pathActual, string pathExpected)
+ {
+ var sut = new ImageCompare();
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ using var actual = new FileStream(absolutePathActual, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var expected = new FileStream(absolutePathExpected, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ Assert.That(sut.ImagesAreEqual(actual, expected), Is.True);
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg0Rgb24)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32)]
+ public void ShouldVerifyThatSkiaSharpImagesAreEqual(string pathActual, string pathExpected)
+ {
+ var sut = new ImageCompare();
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ using var actual = SKBitmap.Decode(absolutePathActual);
+ using var expected = SKBitmap.Decode(absolutePathExpected);
+
+ Assert.That(sut.ImagesAreEqual(actual, expected), Is.True);
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d, null, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d, ResizeOption.DontResize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d, ResizeOption.Resize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.png1Rgba32, 460034, 2.8459701566405187d, 158121, 97.820519165573728d, ResizeOption.DontResize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.png1Rgba32, 460034, 2.8459701566405187d, 158121, 97.820519165573728d, ResizeOption.DontResize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.png1Rgba32, TestFiles.png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.jpg1Rgb24, 0, 0, 0, 0, ResizeOption.DontResize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24, 208890, 1.2922842790329365d, 2101, 1.2997698646408156d, ResizeOption.DontResize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, 203027, 1.25601321422385d, 681, 0.42129618173269651d, ResizeOption.DontResize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.pngBlack4x4px, TestFiles.pngWhite2x2px, 12240, 765, 16, 100.0d, ResizeOption.Resize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.renderedForm1, TestFiles.renderedForm2, 49267623, 60.794204096742348d, 174178, 21.49284304047384d, ResizeOption.Resize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.renderedForm2, TestFiles.renderedForm1, 49267623, 60.794204096742348d, 174178, 21.49284304047384d, ResizeOption.Resize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, 117896, 3.437201166180758d, 30398, 88.623906705539355d, ResizeOption.DontResize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, 0, 0, 0, 0, ResizeOption.DontResize, 15, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.pngPartialTransparent4x4px, TestFiles.pngTransparent4x4px, 2048, 128.0d, 16, 100, ResizeOption.DontResize, 0, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.pngPartialTransparent4x4px, TestFiles.pngTransparent4x4px, 0, 0, 0, 0, ResizeOption.DontResize, 0, TransparencyOptions.IgnoreAlphaChannel)]
+ public void ShouldVerifyThatImagesAreSemiEqual(string pathPic1, string pathPic2, int expectedAbsoluteError, double expectedMeanError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption, int colorShiftTolerance, TransparencyOptions transparencyOptions)
+ {
+ var sut = new ImageCompare(resizeOption, transparencyOptions, colorShiftTolerance);
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+ var diff = sut.CalcDiff(absolutePathPic1, absolutePathPic2);
+
+ Console.WriteLine($"PixelErrorCount: {diff.PixelErrorCount}");
+ Console.WriteLine($"PixelErrorPercentage: {diff.PixelErrorPercentage}");
+ Console.WriteLine($"AbsoluteError: {diff.AbsoluteError}");
+ Console.WriteLine($"MeanError: {diff.MeanError}");
+
+ Assert.That(diff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError");
+ Assert.That(diff.MeanError, Is.EqualTo(expectedMeanError), "MeanError");
+ Assert.That(diff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount");
+ Assert.That(diff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage");
+ }
+
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px)]
+ [TestCase(TestFiles.pngBlack4x4px, TestFiles.pngWhite2x2px)]
+ public void ShouldVerifyThatCalcDiffThrowsOnDifferentImageSizes(string pathPic1, string pathPic2)
+ {
+ var sut = new ImageCompare();
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+ Action action = () => sut.CalcDiff(absolutePathPic1, absolutePathPic2);
+
+ var exception = Assert.Throws(action);
+
+ Assert.That(exception?.Message, Is.EqualTo("Size of images differ."));
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d, null)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d, ResizeOption.DontResize)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d, ResizeOption.Resize)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.png1Rgba32, 460034, 2.8459701566405187d, 158121, 97.820519165573728d, ResizeOption.DontResize)]
+ [TestCase(TestFiles.png1Rgba32, TestFiles.png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.jpg1Rgb24, 0, 0, 0, 0, ResizeOption.DontResize)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24, 208890, 1.2922842790329365d, 2101, 1.2997698646408156d, ResizeOption.DontResize)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, 203027, 1.25601321422385d, 681, 0.42129618173269651d, ResizeOption.DontResize)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngWhite2x2px, 3060, 765, 4, 100.0d, ResizeOption.DontResize)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize)]
+ [TestCase(TestFiles.pngBlack4x4px, TestFiles.pngWhite2x2px, 12240, 765, 16, 100.0d, ResizeOption.Resize)]
+ public void ShouldVerifyThatImageStreamsAreSemiEqual(string pathPic1, string pathPic2, int expectedAbsoluteError, double expectedMeanError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption)
+ {
+ var sut = new ImageCompare(resizeOption);
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+
+ using var pic1 = new FileStream(absolutePathPic1, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var pic2 = new FileStream(absolutePathPic2, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ var diff = sut.CalcDiff(pic1, pic2);
+ Assert.That(diff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError");
+ Assert.That(diff.MeanError, Is.EqualTo(expectedMeanError), "MeanError");
+ Assert.That(diff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount");
+ Assert.That(diff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage");
+ }
+
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px, ResizeOption.Resize, true)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px, ResizeOption.DontResize, true)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, ResizeOption.Resize, true)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngWhite2x2px, ResizeOption.DontResize, false)]
+ public void ShouldCalcDiffMaskSKBitmap(string pathPic1, string pathPic2, ResizeOption resizeOption, bool expectedOutcome)
+ {
+ var sut = new ImageCompare(resizeOption);
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+
+ using var absolutePic1 = SKBitmap.Decode(absolutePathPic1);
+ using var absolutePic2 = SKBitmap.Decode(absolutePathPic2);
+ using var maskImage = sut.CalcDiffMaskImage(absolutePic1, absolutePic2);
+ Assert.That(ImageExtensions.IsImageEntirelyBlack(maskImage, TransparencyOptions.IgnoreAlphaChannel), Is.EqualTo(expectedOutcome));
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, null, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.pngWhite2x2px, TestFiles.pngBlack2x2px, ResizeOption.Resize, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.pngTransparent4x4px, TestFiles.pngPartialTransparent4x4px, ResizeOption.Resize, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, ResizeOption.DontResize, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, ResizeOption.Resize, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.pngWhite2x2px, TestFiles.pngBlack4x4px, ResizeOption.Resize, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.pngBlack4x4px, TestFiles.pngWhite2x2px, ResizeOption.Resize, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.renderedForm1, TestFiles.renderedForm2, ResizeOption.Resize, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.renderedForm2, TestFiles.renderedForm1, ResizeOption.Resize, TransparencyOptions.IgnoreAlphaChannel)]
+ public void CalcDiff(string pathPic1, string pathPic2, ResizeOption resizeOption, TransparencyOptions transparencyOptions)
+ {
+ var sut = new ImageCompare(resizeOption, transparencyOptions);
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+
+ var maskImage1 = sut.CalcDiffMaskImage(absolutePathPic1, absolutePathPic2);
+ Assert.That(ImageExtensions.IsImageEntirelyBlack(maskImage1, transparencyOptions), Is.False);
+
+ using var absolutePic1 = SKBitmap.Decode(absolutePathPic1);
+ using var absolutePic2 = SKBitmap.Decode(absolutePathPic2);
+ var differenceMask = Path.GetTempFileName() + "differenceMask.png";
+
+ using (var fileStreamDifferenceMask = File.Create(differenceMask))
+ using (var maskImage = sut.CalcDiffMaskImage(absolutePic1, absolutePic2))
+ {
+ Assert.That(ImageExtensions.IsImageEntirelyBlack(maskImage, transparencyOptions), Is.False);
+ SaveAsPng(maskImage, fileStreamDifferenceMask);
+ }
+
+ var maskedDiff = Compare.CalcDiff(absolutePathPic1, absolutePathPic2, differenceMask, resizeOption, 0, transparencyOptions);
+ File.Delete(differenceMask);
+
+ Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(0), "AbsoluteError");
+ Assert.That(maskedDiff.MeanError, Is.EqualTo(0), "MeanError");
+ Assert.That(maskedDiff.PixelErrorCount, Is.EqualTo(0), "PixelErrorCount");
+ Assert.That(maskedDiff.PixelErrorPercentage, Is.EqualTo(0), "PixelErrorPercentage");
+ }
+
+ private static void SaveAsPng(SKBitmap maskImage, FileStream fileStreamDifferenceMask)
+ {
+ var encodedData = maskImage.Encode(SKEncodedImageFormat.Png, 100);
+ encodedData.SaveTo(fileStreamDifferenceMask);
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24, 0, 0, 0, 0, ResizeOption.DontResize, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24, 0, 0, 0, 0, ResizeOption.Resize, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngWhite2x2px, 0, 0, 0, 0, ResizeOption.Resize, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.pngTransparent4x4px, TestFiles.pngWhite2x2px, 0, 0, 0, 0, ResizeOption.Resize, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.pngTransparent4x4px, TestFiles.pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize, TransparencyOptions.CompareAlphaChannel)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24, 0, 0, 0, 0, ResizeOption.DontResize, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24, 0, 0, 0, 0, ResizeOption.Resize, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngWhite2x2px, 0, 0, 0, 0, ResizeOption.Resize, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.pngTransparent4x4px, TestFiles.pngWhite2x2px, 0, 0, 0, 0, ResizeOption.Resize, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.pngTransparent4x4px, TestFiles.pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize, TransparencyOptions.IgnoreAlphaChannel)]
+ public void ShouldCalcDiffMaskSKBitmapAndUseOutcome(string pathPic1, string pathPic2, int expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption, TransparencyOptions transparencyOptions)
+ {
+ var sut = new ImageCompare(resizeOption, transparencyOptions);
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+ var differenceMaskPicPath = Path.GetTempFileName() + "differenceMask.png";
+
+ using var absolutePic1 = SKBitmap.Decode(absolutePathPic1);
+ using var absolutePic2 = SKBitmap.Decode(absolutePathPic2);
+
+ using (var fileStreamDifferenceMask = File.Create(differenceMaskPicPath))
+ using (var maskImage = sut.CalcDiffMaskImage(absolutePic1, absolutePic2))
+ {
+ SaveAsPng(maskImage, fileStreamDifferenceMask);
+ }
+
+ using var differenceMaskPic = SKBitmap.Decode(differenceMaskPicPath);
+ var maskedDiff = sut.CalcDiff(absolutePic1, absolutePic2, differenceMaskPic);
+ 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");
+ }
+
+ [TestCase(TestFiles.pngWhite2x2px, TestFiles.pngBlack2x2px, TestFiles.pngTransparent4x4px, 765, 12240, 16, 100d, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.pngWhite2x2px, TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, 765, 12240, 16, 100d, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, TestFiles.pngBlack2x2px, 0, 0, 0, 0, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.pngBlack4x4px, TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px, 0, 0, 0, 0, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift1, TestFiles.pngBlack2x2px, 0, 0, 0, 0, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, TestFiles.pngBlack2x2px, 0, 0, 0, 0, ResizeOption.Resize, 20)]
+ public void ShouldUseDiffMask(string pathPic1, string pathPic2, string pathPic3, double expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption, int colorShiftTolerance)
+ {
+ var sut = new ImageCompare(resizeOption, TransparencyOptions.CompareAlphaChannel, colorShiftTolerance);
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+ var differenceMaskPic = Path.Combine(AppContext.BaseDirectory, pathPic3);
+ using var pic1 = SKBitmap.Decode(absolutePathPic1);
+ using var pic2 = SKBitmap.Decode(absolutePathPic2);
+ using var maskPic = SKBitmap.Decode(differenceMaskPic);
+
+ var maskedDiff = sut.CalcDiff(pic1, pic2, maskPic);
+
+ Assert.That(maskedDiff.MeanError, Is.EqualTo(expectedMeanError), "MeanError");
+ Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError");
+ Assert.That(maskedDiff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount");
+ Assert.That(maskedDiff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage");
+ }
+
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, TestFiles.pngBlack2x2px)]
+ [TestCase(TestFiles.pngBlack4x4px, TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px)]
+ public void ShouldThrowUsingInvalidImageDimensionsDiffMask(string pathPic1, string pathPic2, string pathPic3)
+ {
+ var sut = new ImageCompare();
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+ var differenceMaskPic = Path.Combine(AppContext.BaseDirectory, pathPic3);
+ using var pic1 = SKBitmap.Decode(absolutePathPic1);
+ using var pic2 = SKBitmap.Decode(absolutePathPic2);
+ using var maskPic = SKBitmap.Decode(differenceMaskPic);
+ Action action = () => sut.CalcDiff(pic1, pic2, maskPic);
+
+ var exception = Assert.Throws(action);
+
+ Assert.That(exception?.Message, Is.EqualTo("Size of images differ."));
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, 0, 0, 0, 0)]
+ public void CalcDiffStreams(string pathPic1, string pathPic2, int expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage)
+ {
+ var sut = new ImageCompare(transparencyOptions: TransparencyOptions.IgnoreAlphaChannel);
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+
+ using var pic1 = new FileStream(absolutePathPic1, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var pic2 = new FileStream(absolutePathPic2, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ using var maskImage = sut.CalcDiffMaskImage(pic1, pic2);
+
+ pic1.Position = 0;
+ pic2.Position = 0;
+
+ var maskedDiff = sut.CalcDiff(pic1, pic2, maskImage);
+ 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");
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, TransparencyOptions.CompareAlphaChannel, 0)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, TransparencyOptions.CompareAlphaChannel, 20)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, TransparencyOptions.IgnoreAlphaChannel, 0)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, TransparencyOptions.IgnoreAlphaChannel, 20)]
+ [TestCase(TestFiles.pngTransparent4x4px, TestFiles.pngPartialTransparent4x4px, TransparencyOptions.IgnoreAlphaChannel, 0)]
+ [TestCase(TestFiles.pngTransparent4x4px, TestFiles.pngPartialTransparent4x4px, TransparencyOptions.CompareAlphaChannel, 0)]
+ public void CalcDiffMaskImage_WhenSupplyingDiffMaskOfTwoImagesByFilePath_NoDifferences(string image1RelativePath, string image2RelativePath, TransparencyOptions transparencyOptions, int colorShiftTolerance)
+ {
+ var sut = new ImageCompare(ResizeOption.DontResize, transparencyOptions, colorShiftTolerance);
+ var image1Path = Path.Combine(AppContext.BaseDirectory, image1RelativePath);
+ var image2Path = Path.Combine(AppContext.BaseDirectory, image2RelativePath);
+ var diffMask1Path = Path.GetTempFileName() + "differenceMask.png";
+
+ using (var diffMask1Stream = File.Create(diffMask1Path))
+ {
+ using var diffMask1Image = sut.CalcDiffMaskImage(image1Path, image2Path);
+ ImageExtensions.SaveAsPng(diffMask1Image, diffMask1Stream);
+ }
+
+ using var diffMask2Image = sut.CalcDiffMaskImage(image1Path, image2Path, diffMask1Path);
+
+ using (var diffMask2Stream = File.Create(diffMask1Path))
+ {
+ ImageExtensions.SaveAsPng(diffMask2Image, diffMask2Stream);
+ }
+
+ Assert.That(ImageExtensions.IsImageEntirelyBlack(diffMask2Image, transparencyOptions), Is.True);
+
+ File.Delete(diffMask1Path);
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32)]
+ public void CalcDiffMaskImage_WhenSupplyingDiffMaskOfTwoImagesByStream_NoDifferences(string image1RelativePath, string image2RelativePath)
+ {
+ var sut = new ImageCompare();
+ var image1Path = Path.Combine(AppContext.BaseDirectory, image1RelativePath);
+ var image2Path = Path.Combine(AppContext.BaseDirectory, image2RelativePath);
+ var diffMask1Path = Path.GetTempFileName() + "differenceMask.png";
+
+ using var image1Stream = new FileStream(image1Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var image2Stream = new FileStream(image2Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ using (var diffMask1Stream = File.Create(diffMask1Path))
+ {
+ using var diffMask1Image = sut.CalcDiffMaskImage(image1Stream, image2Stream);
+ ImageExtensions.SaveAsPng(diffMask1Image, diffMask1Stream);
+ }
+
+ image1Stream.Position = 0;
+ image2Stream.Position = 0;
+
+ using (var diffMask1Stream = new FileStream(diffMask1Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ {
+ diffMask1Stream.Position = 0;
+ using var diffMask2Image = sut.CalcDiffMaskImage(image1Stream, image2Stream, diffMask1Stream);
+ Assert.That(ImageExtensions.IsImageEntirelyBlack(diffMask2Image, TransparencyOptions.IgnoreAlphaChannel), Is.True);
+ }
+
+ File.Delete(diffMask1Path);
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32)]
+ public void CalcDiffMaskImage_WhenSupplyingDiffMaskOfTwoImagesByImage_NoDifferences(string image1RelativePath, string image2RelativePath)
+ {
+ var sut = new ImageCompare();
+ var image1Path = Path.Combine(AppContext.BaseDirectory, image1RelativePath);
+ var image2Path = Path.Combine(AppContext.BaseDirectory, image2RelativePath);
+
+ using var image1 = SKBitmap.Decode(image1Path);
+ using var image2 = SKBitmap.Decode(image2Path);
+
+ using var diffMask1Image = sut.CalcDiffMaskImage(image1, image2);
+ using var diffMask2Image = sut.CalcDiffMaskImage(image1, image2, diffMask1Image);
+
+ Assert.That(ImageExtensions.IsImageEntirelyBlack(diffMask2Image, TransparencyOptions.IgnoreAlphaChannel), Is.True);
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png1Rgba32)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.png1Rgba32)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2)]
+ [TestCase(TestFiles.pngTransparent4x4px, TestFiles.pngPartialTransparent4x4px)]
+ public void ShouldVerifyThatImagesAreNotEqual(string pathActual, string pathExpected)
+ {
+ var sut = new ImageCompare();
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ Assert.That(sut.ImagesAreEqual(absolutePathActual, absolutePathExpected), Is.False);
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png1Rgba32)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.png1Rgba32)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2)]
+ [TestCase(TestFiles.pngTransparent4x4px, TestFiles.pngPartialTransparent4x4px)]
+ public void ShouldVerifyThatImageStreamAreNotEqual(string pathActual, string pathExpected)
+ {
+ var sut = new ImageCompare();
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ using var actual = new FileStream(absolutePathActual, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var expected = new FileStream(absolutePathExpected, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ Assert.That(sut.ImagesAreEqual(actual, expected), Is.False);
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.pngBlack2x2px, TransparencyOptions.IgnoreAlphaChannel)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.pngBlack2x2px, TransparencyOptions.CompareAlphaChannel)]
+ public void ShouldVerifyThatImageWithDifferentSizeThrows(string pathPic1, string pathPic2, TransparencyOptions transparencyOptions)
+ {
+ var sut = new ImageCompare(ResizeOption.DontResize, transparencyOptions);
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+ Action action = () => sut.CalcDiff(absolutePathPic1, absolutePathPic2);
+
+ var exception = Assert.Throws(action);
+
+ Assert.That(exception?.Message, Is.EqualTo("Size of images differ."));
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, TestFiles.pngBlack2x2px)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.pngBlack2x2px, TestFiles.png0Rgba32)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.png0Rgba32, TestFiles.png0Rgba32)]
+ [TestCase(TestFiles.pngPartialTransparent4x4px, TestFiles.png0Rgba32, TestFiles.png0Rgba32)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, TestFiles.pngPartialTransparent4x4px)]
+ public void ShouldVerifyThatImageWithDifferentSizeThrows(string pathPic1, string pathPic2, string pathPic3)
+ {
+ var sut = new ImageCompare();
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+ var absolutePathPic3 = Path.Combine(AppContext.BaseDirectory, pathPic3);
+ Action action = () => sut.CalcDiff(absolutePathPic1, absolutePathPic2, absolutePathPic3);
+
+ var exception = Assert.Throws(action);
+
+ Assert.That(exception?.Message, Is.EqualTo("Size of images differ."));
+ }
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompareTestNunit/SkiaSharpStaticCompareTests.cs b/SkiaSharpCompareTestNunit/SkiaSharpStaticCompareTests.cs
new file mode 100644
index 0000000..94955c5
--- /dev/null
+++ b/SkiaSharpCompareTestNunit/SkiaSharpStaticCompareTests.cs
@@ -0,0 +1,477 @@
+using Codeuctivity.SkiaSharpCompare;
+using NUnit.Framework;
+using SkiaSharp;
+using System;
+using System.IO;
+
+namespace SkiaSharpCompareTestNunit
+{
+ public class SkiaSharpStaticCompareTests
+ {
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg0Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.jpg0Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.jpg1Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.pngBlack2x2px, false)]
+ public void ShouldVerifyThatImagesFromFilePathSizeAreEqual(string pathActual, string pathExpected, bool expectedOutcome)
+ {
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ Assert.That(Compare.ImagesHaveEqualSize(absolutePathActual, absolutePathExpected), Is.EqualTo(expectedOutcome));
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg0Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.jpg0Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.jpg1Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.pngBlack2x2px, false)]
+ public void ShouldVerifyThatImagesSizeAreEqual(string pathActual, string pathExpected, bool expectedOutcome)
+ {
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ using var actual = SKBitmap.Decode(absolutePathActual);
+ using var expected = SKBitmap.Decode(absolutePathExpected);
+
+ Assert.That(Compare.ImagesHaveEqualSize(absolutePathActual, absolutePathExpected), Is.EqualTo(expectedOutcome));
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg0Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.jpg0Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.jpg1Rgb24, true)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.pngBlack2x2px, false)]
+ public void ShouldVerifyThatImageStreamsSizeAreEqual(string pathActual, string pathExpected, bool expectedOutcome)
+ {
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ using var actual = new FileStream(absolutePathActual, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var expected = new FileStream(absolutePathExpected, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ Assert.That(Compare.ImagesHaveEqualSize(absolutePathActual, absolutePathExpected), Is.EqualTo(expectedOutcome));
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg0Rgb24, 0)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, 0)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, 15)]
+ public void ShouldVerifyThatImagesAreEqual(string pathActual, string pathExpected, int colorShiftTolerance)
+ {
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ Assert.That(Compare.ImagesAreEqual(absolutePathActual, absolutePathExpected, pixelColorShiftTolerance: colorShiftTolerance), Is.True);
+ }
+
+ [Test]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px, ResizeOption.Resize, true)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, ResizeOption.Resize, true)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, ResizeOption.DontResize, false)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, ResizeOption.DontResize, false)]
+ public void ShouldVerifyThatImagesWithDifferentSizeAreEqual(string pathActual, string pathExpected, ResizeOption resizeOption, bool expectedResult)
+ {
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ Assert.That(Compare.ImagesAreEqual(absolutePathActual, absolutePathExpected, resizeOption), Is.EqualTo(expectedResult));
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg0Rgb24)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32)]
+ public void ShouldVerifyThatImageStreamsAreEqual(string pathActual, string pathExpected)
+ {
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ using var actual = new FileStream(absolutePathActual, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var expected = new FileStream(absolutePathExpected, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ Assert.That(Compare.ImagesAreEqual(actual, expected), Is.True);
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg0Rgb24)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32)]
+ public void ShouldVerifyThatSkiaSharpImagesAreEqual(string pathActual, string pathExpected)
+ {
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ using var actual = SKBitmap.Decode(absolutePathActual);
+ using var expected = SKBitmap.Decode(absolutePathExpected);
+
+ Assert.That(Compare.ImagesAreEqual(actual, expected), Is.True);
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d, null, 0)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d, ResizeOption.DontResize, 0)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.png1Rgba32, 460034, 2.8459701566405187d, 158121, 97.820519165573728d, ResizeOption.DontResize, 0)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.png1Rgba32, 460034, 2.8459701566405187d, 158121, 97.820519165573728d, ResizeOption.DontResize, 0)]
+ [TestCase(TestFiles.png1Rgba32, TestFiles.png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize, 0)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.jpg1Rgb24, 0, 0, 0, 0, ResizeOption.DontResize, 0)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24, 208890, 1.2922842790329365d, 2101, 1.2997698646408156d, ResizeOption.DontResize, 0)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, 203027, 1.25601321422385d, 681, 0.42129618173269651d, ResizeOption.DontResize, 0)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.pngBlack4x4px, TestFiles.pngWhite2x2px, 12240, 765, 16, 100.0d, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.renderedForm1, TestFiles.renderedForm2, 49267623, 60.794204096742348d, 174178, 21.49284304047384d, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.renderedForm2, TestFiles.renderedForm1, 49267623, 60.794204096742348d, 174178, 21.49284304047384d, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, 117896, 3.437201166180758d, 30398, 88.623906705539355d, ResizeOption.DontResize, 0)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, 0, 0, 0, 0, ResizeOption.DontResize, 15)]
+ public void ShouldVerifyThatImagesAreSemiEqual(string pathPic1, string pathPic2, int expectedAbsoluteError, double expectedMeanError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption, int colorShiftTolerance)
+ {
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+ var diff = Compare.CalcDiff(absolutePathPic1, absolutePathPic2, resizeOption, colorShiftTolerance);
+
+ Console.WriteLine($"PixelErrorCount: {diff.PixelErrorCount}");
+ Console.WriteLine($"PixelErrorPercentage: {diff.PixelErrorPercentage}");
+ Console.WriteLine($"AbsoluteError: {diff.AbsoluteError}");
+ Console.WriteLine($"MeanError: {diff.MeanError}");
+
+ Assert.That(diff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError");
+ Assert.That(diff.MeanError, Is.EqualTo(expectedMeanError), "MeanError");
+ Assert.That(diff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount");
+ Assert.That(diff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage");
+ }
+
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px)]
+ [TestCase(TestFiles.pngBlack4x4px, TestFiles.pngWhite2x2px)]
+ public void ShouldVerifyThatCalcDiffThrowsOnDifferentImageSizes(string pathPic1, string pathPic2)
+ {
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+ Action action = () => Compare.CalcDiff(absolutePathPic1, absolutePathPic2, ResizeOption.DontResize);
+
+ var exception = Assert.Throws(action);
+
+ Assert.That(exception?.Message, Is.EqualTo("Size of images differ."));
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d, null)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d, ResizeOption.DontResize)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32, 461891, 2.8574583652965777d, 158087, 97.799485288659028d, ResizeOption.Resize)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.png1Rgba32, 460034, 2.8459701566405187d, 158121, 97.820519165573728d, ResizeOption.DontResize)]
+ [TestCase(TestFiles.png1Rgba32, TestFiles.png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.jpg1Rgb24, 0, 0, 0, 0, ResizeOption.DontResize)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24, 208890, 1.2922842790329365d, 2101, 1.2997698646408156d, ResizeOption.DontResize)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, 203027, 1.25601321422385d, 681, 0.42129618173269651d, ResizeOption.DontResize)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngWhite2x2px, 3060, 765, 4, 100.0d, ResizeOption.DontResize)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize)]
+ [TestCase(TestFiles.pngBlack4x4px, TestFiles.pngWhite2x2px, 12240, 765, 16, 100.0d, ResizeOption.Resize)]
+ public void ShouldVerifyThatImageStreamsAreSemiEqual(string pathPic1, string pathPic2, int expectedAbsoluteError, double expectedMeanError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption)
+ {
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+
+ using var pic1 = new FileStream(absolutePathPic1, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var pic2 = new FileStream(absolutePathPic2, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ var diff = Compare.CalcDiff(pic1, pic2, resizeOption);
+ Assert.That(diff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError");
+ Assert.That(diff.MeanError, Is.EqualTo(expectedMeanError), "MeanError");
+ Assert.That(diff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount");
+ Assert.That(diff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage");
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, 0, 0, 0, 0, null)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, 0, 0, 0, 0, ResizeOption.Resize)]
+ [TestCase(TestFiles.pngWhite2x2px, TestFiles.pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize)]
+ [TestCase(TestFiles.pngBlack4x4px, TestFiles.pngWhite2x2px, 0, 0, 0, 0, ResizeOption.Resize)]
+ [TestCase(TestFiles.renderedForm1, TestFiles.renderedForm2, 0, 0, 0, 0, ResizeOption.Resize)]
+ [TestCase(TestFiles.renderedForm2, TestFiles.renderedForm1, 0, 0, 0, 0, ResizeOption.Resize)]
+ public void DiffMask(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 differenceMask = Path.GetTempFileName() + "differenceMask.png";
+
+ using (var fileStreamDifferenceMask = File.Create(differenceMask))
+ using (var maskImage = Compare.CalcDiffMaskImage(absolutePathPic1, absolutePathPic2, resizeOption))
+ {
+ SaveAsPng(maskImage, fileStreamDifferenceMask);
+ }
+
+ var maskedDiff = Compare.CalcDiff(absolutePathPic1, absolutePathPic2, differenceMask, resizeOption);
+ File.Delete(differenceMask);
+
+ 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");
+ }
+
+ private static void SaveAsPng(SKBitmap maskImage, FileStream fileStreamDifferenceMask)
+ {
+ var encodedData = maskImage.Encode(SKEncodedImageFormat.Png, 100);
+ encodedData.SaveTo(fileStreamDifferenceMask);
+ }
+
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px, ResizeOption.Resize)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px, ResizeOption.DontResize)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, ResizeOption.Resize)]
+ public void ShouldCalcDiffMaskSKBitmap(string pathPic1, string pathPic2, ResizeOption resizeOption)
+ {
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+
+ using var absolutePic1 = SKBitmap.Decode(absolutePathPic1);
+ using var absolutePic2 = SKBitmap.Decode(absolutePathPic2);
+ using var maskImage = Compare.CalcDiffMaskImage(absolutePic1, absolutePic2, resizeOption);
+ Assert.That(ImageExtensions.IsImageEntirelyBlack(maskImage, TransparencyOptions.IgnoreAlphaChannel));
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24, 0, 0, 0, 0, ResizeOption.DontResize)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24, 0, 0, 0, 0, ResizeOption.Resize)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize)]
+ public void ShouldCalcDiffMaskSKBitmapAndUseOutcome(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 = SKBitmap.Decode(absolutePathPic1);
+ using var absolutePic2 = SKBitmap.Decode(absolutePathPic2);
+
+ using (var fileStreamDifferenceMask = File.Create(differenceMaskPicPath))
+ using (var maskImage = Compare.CalcDiffMaskImage(absolutePic1, absolutePic2, resizeOption))
+ {
+ SaveAsPng(maskImage, fileStreamDifferenceMask);
+ }
+
+ using var differenceMaskPic = SKBitmap.Decode(differenceMaskPicPath);
+ var maskedDiff = Compare.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");
+ }
+
+ [TestCase(TestFiles.pngWhite2x2px, TestFiles.pngBlack2x2px, TestFiles.pngTransparent4x4px, 765, 12240, 16, 100d, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.pngWhite2x2px, TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, 765, 12240, 16, 100d, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, TestFiles.pngBlack2x2px, 0, 0, 0, 0, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.pngBlack4x4px, TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px, 0, 0, 0, 0, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift1, TestFiles.pngBlack2x2px, 0, 0, 0, 0, ResizeOption.Resize, 0)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, TestFiles.pngBlack2x2px, 0, 0, 0, 0, ResizeOption.Resize, 20)]
+ public void ShouldUseDiffMask(string pathPic1, string pathPic2, string pathPic3, double expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption, int colorShiftTolerance)
+ {
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+ var differenceMaskPic = Path.Combine(AppContext.BaseDirectory, pathPic3);
+ using var pic1 = SKBitmap.Decode(absolutePathPic1);
+ using var pic2 = SKBitmap.Decode(absolutePathPic2);
+ using var maskPic = SKBitmap.Decode(differenceMaskPic);
+
+ var maskedDiff = Compare.CalcDiff(pic1, pic2, maskPic, resizeOption, colorShiftTolerance);
+
+ Assert.That(maskedDiff.MeanError, Is.EqualTo(expectedMeanError), "MeanError");
+ Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError");
+ Assert.That(maskedDiff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount");
+ Assert.That(maskedDiff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage");
+ }
+
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.pngBlack4x4px, TestFiles.pngBlack2x2px)]
+ [TestCase(TestFiles.pngBlack4x4px, TestFiles.pngBlack2x2px, TestFiles.pngBlack2x2px)]
+ public void ShouldThrowUsingInvalidImageDimensionsDiffMask(string pathPic1, string pathPic2, string pathPic3)
+ {
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+ var differenceMaskPic = Path.Combine(AppContext.BaseDirectory, pathPic3);
+ using var pic1 = SKBitmap.Decode(absolutePathPic1);
+ using var pic2 = SKBitmap.Decode(absolutePathPic2);
+ using var maskPic = SKBitmap.Decode(differenceMaskPic);
+ Action action = () => Compare.CalcDiff(pic1, pic2, maskPic, ResizeOption.DontResize);
+
+ var exception = Assert.Throws(action);
+
+ Assert.That(exception?.Message, Is.EqualTo("Size of images differ."));
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, 0, 0, 0, 0)]
+ public void DiffMaskStreams(string pathPic1, string pathPic2, int expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage)
+ {
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+
+ using var pic1 = new FileStream(absolutePathPic1, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var pic2 = new FileStream(absolutePathPic2, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ using var maskImage = Compare.CalcDiffMaskImage(pic1, pic2);
+
+ pic1.Position = 0;
+ pic2.Position = 0;
+
+ var maskedDiff = Compare.CalcDiff(pic1, pic2, maskImage);
+ 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");
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, 0, false)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, 20, true)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, 0, false)]
+ public void CalcDiffMaskImage_WhenSupplyingDiffMaskOfTwoImagesByFilePath_NoDifferences(string image1RelativePath, string image2RelativePath, int colorShiftTolerance, bool expectIsImageEntirelyBlack)
+ {
+ var image1Path = Path.Combine(AppContext.BaseDirectory, image1RelativePath);
+ var image2Path = Path.Combine(AppContext.BaseDirectory, image2RelativePath);
+ var diffMask1Path = Path.GetTempFileName() + "differenceMask.png";
+
+ using (var diffMask1Stream = File.Create(diffMask1Path))
+ {
+ using var diffMask1Image = Compare.CalcDiffMaskImage(image1Path, image2Path, ResizeOption.DontResize, colorShiftTolerance);
+ ImageExtensions.SaveAsPng(diffMask1Image, diffMask1Stream);
+ Assert.That(ImageExtensions.IsImageEntirelyBlack(diffMask1Image, TransparencyOptions.IgnoreAlphaChannel), Is.EqualTo(expectIsImageEntirelyBlack));
+ }
+
+ using var diffMask2Image = Compare.CalcDiffMaskImage(image1Path, image2Path, diffMask1Path);
+
+ using (var diffMask2Stream = File.Create(diffMask1Path))
+ {
+ ImageExtensions.SaveAsPng(diffMask2Image, diffMask2Stream);
+ }
+
+ Assert.That(ImageExtensions.IsImageEntirelyBlack(diffMask2Image, TransparencyOptions.IgnoreAlphaChannel));
+
+ File.Delete(diffMask1Path);
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32)]
+ public void CalcDiffMaskImage_WhenSupplyingDiffMaskOfTwoImagesByStream_NoDifferences(string image1RelativePath, string image2RelativePath)
+ {
+ var image1Path = Path.Combine(AppContext.BaseDirectory, image1RelativePath);
+ var image2Path = Path.Combine(AppContext.BaseDirectory, image2RelativePath);
+ var diffMask1Path = Path.GetTempFileName() + "differenceMask.png";
+
+ using var image1Stream = new FileStream(image1Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var image2Stream = new FileStream(image2Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ using (var diffMask1Stream = File.Create(diffMask1Path))
+ {
+ using var diffMask1Image = Compare.CalcDiffMaskImage(image1Stream, image2Stream);
+ ImageExtensions.SaveAsPng(diffMask1Image, diffMask1Stream);
+ }
+
+ image1Stream.Position = 0;
+ image2Stream.Position = 0;
+
+ using (var diffMask1Stream = new FileStream(diffMask1Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ {
+ diffMask1Stream.Position = 0;
+ using var diffMask2Image = Compare.CalcDiffMaskImage(image1Stream, image2Stream, diffMask1Stream);
+ Assert.That(ImageExtensions.IsImageEntirelyBlack(diffMask2Image, TransparencyOptions.IgnoreAlphaChannel), Is.True);
+ }
+
+ File.Delete(diffMask1Path);
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32, TestFiles.png1Rgba32, 0, false)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift1, TestFiles.colorShift2, 15, true)]
+ public void CalcDiffMaskImage_WhenSupplyingDiffMaskOfTwoImagesByImage_NoDifferences(string image1RelativePath, string image2RelativePath, string image3RelativePath, int colorShiftTolerance, bool expectToleranceMaskToEntirelyBlack)
+ {
+ var image1Path = Path.Combine(AppContext.BaseDirectory, image1RelativePath);
+ var image2Path = Path.Combine(AppContext.BaseDirectory, image2RelativePath);
+ var image3Path = Path.Combine(AppContext.BaseDirectory, image3RelativePath);
+
+ using var image1 = SKBitmap.Decode(image1Path);
+ using var image2 = SKBitmap.Decode(image2Path);
+ using var image3 = SKBitmap.Decode(image3Path);
+
+ using var diffMask1Image = Compare.CalcDiffMaskImage(image1, image2);
+
+ using var diffMask2Image = Compare.CalcDiffMaskImage(image1, image3, diffMask1Image, pixelColorShiftTolerance: colorShiftTolerance);
+
+ Assert.That(ImageExtensions.IsImageEntirelyBlack(diffMask1Image, TransparencyOptions.IgnoreAlphaChannel), Is.EqualTo(expectToleranceMaskToEntirelyBlack));
+ Assert.That(ImageExtensions.IsImageEntirelyBlack(diffMask2Image, TransparencyOptions.IgnoreAlphaChannel), Is.True);
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, 0)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, 15)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2, 15)]
+ public void CalcDiffMaskImage_ToleranceColorShift_NoDifferences(string image1RelativePath, string image2RelativePath, int colorShiftTolerance)
+ {
+ var image1Path = Path.Combine(AppContext.BaseDirectory, image1RelativePath);
+ var image2Path = Path.Combine(AppContext.BaseDirectory, image2RelativePath);
+
+ using var image1 = SKBitmap.Decode(image1Path);
+ using var image2 = SKBitmap.Decode(image2Path);
+
+ using var diffMask1Image = Compare.CalcDiffMaskImage(image1, image2, pixelColorShiftTolerance: colorShiftTolerance);
+
+ Assert.That(ImageExtensions.IsImageEntirelyBlack(diffMask1Image, TransparencyOptions.IgnoreAlphaChannel), Is.True);
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png1Rgba32)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.png1Rgba32)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2)]
+ public void ShouldVerifyThatImagesAreNotEqual(string pathActual, string pathExpected)
+ {
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ Assert.That(Compare.ImagesAreEqual(absolutePathActual, absolutePathExpected), Is.False);
+ }
+
+ [Test]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.jpg1Rgb24)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png1Rgba32)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png1Rgba32)]
+ [TestCase(TestFiles.jpg0Rgb24, TestFiles.png0Rgba32)]
+ [TestCase(TestFiles.jpg1Rgb24, TestFiles.png1Rgba32)]
+ [TestCase(TestFiles.colorShift1, TestFiles.colorShift2)]
+ public void ShouldVerifyThatImageStreamAreNotEqual(string pathActual, string pathExpected)
+ {
+ var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual);
+ var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected);
+
+ using var actual = new FileStream(absolutePathActual, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ using var expected = new FileStream(absolutePathExpected, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+
+ Assert.That(Compare.ImagesAreEqual(actual, expected), Is.False);
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.pngBlack2x2px)]
+ public void ShouldVerifyThatImageWithDifferentSizeThrows(string pathPic1, string pathPic2)
+ {
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+ Action action = () => Compare.CalcDiff(absolutePathPic1, absolutePathPic2);
+
+ var exception = Assert.Throws(action);
+
+ Assert.That(exception?.Message, Is.EqualTo("Size of images differ."));
+ }
+
+ [TestCase(TestFiles.png0Rgba32, TestFiles.png0Rgba32, TestFiles.pngBlack2x2px)]
+ [TestCase(TestFiles.png0Rgba32, TestFiles.pngBlack2x2px, TestFiles.png0Rgba32)]
+ [TestCase(TestFiles.pngBlack2x2px, TestFiles.png0Rgba32, TestFiles.png0Rgba32)]
+ public void ShouldVerifyThatImageWithDifferentSizeThrows(string pathPic1, string pathPic2, string pathPic3)
+ {
+ var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1);
+ var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2);
+ var absolutePathPic3 = Path.Combine(AppContext.BaseDirectory, pathPic3);
+ Action action = () => Compare.CalcDiff(absolutePathPic1, absolutePathPic2, absolutePathPic3);
+
+ var exception = Assert.Throws(action);
+
+ Assert.That(exception?.Message, Is.EqualTo("Size of images differ."));
+ }
+ }
+}
\ No newline at end of file
diff --git a/SkiaSharpCompareTestNunit/TestData/BlackDoubleSize.png b/SkiaSharpCompareTestNunit/TestData/BlackDoubleSize.png
new file mode 100644
index 0000000..9a5886b
Binary files /dev/null and b/SkiaSharpCompareTestNunit/TestData/BlackDoubleSize.png differ
diff --git a/SkiaSharpCompareTestNunit/TestData/ColorShift1.png b/SkiaSharpCompareTestNunit/TestData/ColorShift1.png
new file mode 100644
index 0000000..fa349b8
Binary files /dev/null and b/SkiaSharpCompareTestNunit/TestData/ColorShift1.png differ
diff --git a/SkiaSharpCompareTestNunit/TestData/ColorShift2.png b/SkiaSharpCompareTestNunit/TestData/ColorShift2.png
new file mode 100644
index 0000000..7cfe42a
Binary files /dev/null and b/SkiaSharpCompareTestNunit/TestData/ColorShift2.png differ
diff --git a/SkiaSharpCompareTestNunit/TestData/HC007-Test-02-3-OxPt.html1.png b/SkiaSharpCompareTestNunit/TestData/HC007-Test-02-3-OxPt.html1.png
new file mode 100644
index 0000000..b860ccd
Binary files /dev/null and b/SkiaSharpCompareTestNunit/TestData/HC007-Test-02-3-OxPt.html1.png differ
diff --git a/SkiaSharpCompareTestNunit/TestData/HC007-Test-02-3-OxPt.html2.png b/SkiaSharpCompareTestNunit/TestData/HC007-Test-02-3-OxPt.html2.png
new file mode 100644
index 0000000..98ac29e
Binary files /dev/null and b/SkiaSharpCompareTestNunit/TestData/HC007-Test-02-3-OxPt.html2.png differ
diff --git a/SkiaSharpCompareTestNunit/TestData/createToleranceMaskFromDiff.png b/SkiaSharpCompareTestNunit/TestData/createToleranceMaskFromDiff.png
new file mode 100644
index 0000000..24ab8f4
Binary files /dev/null and b/SkiaSharpCompareTestNunit/TestData/createToleranceMaskFromDiff.png differ
diff --git a/SkiaSharpCompareTestNunit/TestData/imageWithGpsMetadata.jpg b/SkiaSharpCompareTestNunit/TestData/imageWithGpsMetadata.jpg
new file mode 100644
index 0000000..4e4c0ff
Binary files /dev/null and b/SkiaSharpCompareTestNunit/TestData/imageWithGpsMetadata.jpg differ
diff --git a/SkiaSharpCompareTestNunit/TestData/imageWithoutGpsMetadata.jpg b/SkiaSharpCompareTestNunit/TestData/imageWithoutGpsMetadata.jpg
new file mode 100644
index 0000000..1313728
Binary files /dev/null and b/SkiaSharpCompareTestNunit/TestData/imageWithoutGpsMetadata.jpg differ
diff --git a/SkiaSharpCompareTestNunit/TestData/pngPartialTransparent4x4px.png b/SkiaSharpCompareTestNunit/TestData/pngPartialTransparent4x4px.png
new file mode 100644
index 0000000..fe044ba
Binary files /dev/null and b/SkiaSharpCompareTestNunit/TestData/pngPartialTransparent4x4px.png differ
diff --git a/SkiaSharpCompareTestNunit/TestData/pngTransparent4x4px.png b/SkiaSharpCompareTestNunit/TestData/pngTransparent4x4px.png
new file mode 100644
index 0000000..c83cef5
Binary files /dev/null and b/SkiaSharpCompareTestNunit/TestData/pngTransparent4x4px.png differ
diff --git a/SkiaSharpCompareTestNunit/TestFiles.cs b/SkiaSharpCompareTestNunit/TestFiles.cs
new file mode 100644
index 0000000..b6b05dc
--- /dev/null
+++ b/SkiaSharpCompareTestNunit/TestFiles.cs
@@ -0,0 +1,25 @@
+namespace SkiaSharpCompareTestNunit
+{
+ internal static class TestFiles
+ {
+ public const string jpg0Rgb24 = "./../../../TestData/Calc0.jpg";
+ public const string jpg1Rgb24 = "./../../../TestData/Calc1.jpg";
+ public const string png0Rgba32 = "./../../../TestData/Calc0.png";
+ public const string png1Rgba32 = "./../../../TestData/Calc1.png";
+ public const string pngBlack2x2px = "./../../../TestData/Black.png";
+ public const string pngBlack4x4px = "./../../../TestData/BlackDoubleSize.png";
+ public const string pngWhite2x2px = "./../../../TestData/White.png";
+ public const string pngTransparent4x4px = "./../../../TestData/pngTransparent4x4px.png";
+ public const string pngPartialTransparent4x4px = "./../../../TestData/pngPartialTransparent4x4px.png";
+ public const string renderedForm1 = "./../../../TestData/HC007-Test-02-3-OxPt.html1.png";
+ public const string renderedForm2 = "./../../../TestData/HC007-Test-02-3-OxPt.html2.png";
+ public const string colorShift1 = "./../../../TestData/ColorShift1.png";
+ public const string colorShift2 = "./../../../TestData/ColorShift2.png";
+
+ // These changes are related to https://github.com/nextcloud/android/issues/6248#issuecomment-1207315271
+ public const string imageWithoutGpsMetadata = "./../../../TestData/imageWithoutGpsMetadata.jpg";
+
+ public const string imageWithGpsMetadata = "./../../../TestData/imageWithGpsMetadata.jpg";
+ public const string createToleranceMaskFromDiff = "./../../../TestData/createToleranceMaskFromDiff.png";
+ }
+}
\ No newline at end of file
diff --git a/exclusion.dic b/exclusion.dic
new file mode 100644
index 0000000..b510bd0
--- /dev/null
+++ b/exclusion.dic
@@ -0,0 +1,3 @@
+Skia
+Codeuctivity
+Calc