├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── cla.yml │ ├── dotnet.yml │ └── stale.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json ├── tasks.json └── updateNuget.sh ├── ImageSharpCompare.sln ├── ImageSharpCompare ├── CompareResult.cs ├── ICompareResult.cs ├── ImageSharpCompare.cs ├── ImageSharpCompare.csproj ├── ImageSharpCompare.sln ├── ImageSharpCompare.snk ├── ImageSharpCompareException.cs ├── ImageSharpPixelTypeConverter.cs ├── NugetIcon.png ├── ResizeOption.cs └── docs │ └── nugetReadme.md ├── ImageSharpCompareTestNunit ├── AssertDisposeBehavior.cs ├── ImageSharpCompareTest.cs ├── ImageSharpCompareTestNunit.csproj ├── ImageSharpPixelTypeConverterTests.cs └── TestData │ ├── Black.png │ ├── BlackDoubleSize.png │ ├── Calc0.jpg │ ├── Calc0.png │ ├── Calc1.jpg │ ├── Calc1.png │ ├── ColorShift1.png │ ├── ColorShift2.png │ ├── HC007-Test-02-3-OxPt.html1.png │ ├── HC007-Test-02-3-OxPt.html2.png │ ├── White.png │ ├── differenceMask.png │ └── pngTransparent2x2px.png ├── LICENSE ├── README.md ├── cla.md ├── signatures └── version1 │ └── cla.json └── testenvironments.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # CA1303: Do not pass literals as localized parameters 4 | dotnet_diagnostic.CA1303.severity = none 5 | 6 | # CA2000: Dispose objects before losing scope 7 | dotnet_diagnostic.CA2000.severity = suggestion 8 | 9 | # var preferences 10 | csharp_style_var_elsewhere = true:warning 11 | csharp_style_var_for_built_in_types = true:warning 12 | csharp_style_var_when_type_is_apparent = true:warning 13 | 14 | [*.{cs,vb}] 15 | tab_width=4 16 | indent_size=4 17 | 18 | # Code-block preferences - reflecting codacy defaults 19 | csharp_prefer_braces = true:warning 20 | 21 | # IDE0011: Add braces - reflecting codacy defaults 22 | dotnet_diagnostic.IDE0011.severity = warning 23 | dotnet_diagnostic.IDE0060.severity = error -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [Codeuctivity] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: stesee 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "docker" 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | rebase-strategy: auto 13 | 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: weekly 18 | rebase-strategy: auto 19 | 20 | - package-ecosystem: "npm" 21 | directory: "/" 22 | schedule: 23 | interval: weekly 24 | rebase-strategy: auto 25 | 26 | - package-ecosystem: "nuget" 27 | directory: "/" 28 | schedule: 29 | interval: "daily" 30 | ignore: 31 | - dependency-name: "nunit" 32 | - dependency-name: "coverlet.collector" 33 | - dependency-name: "SonarAnalyzer.CSharp" 34 | - dependency-name: "AngleSharp" 35 | - dependency-name: "Microsoft.NET.Test.Sdk" 36 | - dependency-name: "Microsoft.AspNetCore.Mvc.Testing" 37 | - dependency-name: "Moq" 38 | - dependency-name: "xunit" 39 | - dependency-name: "xunit.runner.visualstudio" 40 | - dependency-name: "MSTest.TestAdapter" 41 | - dependency-name: "MSTest.TestFramework" 42 | - dependency-name: "NUnit3TestAdapter" -------------------------------------------------------------------------------- /.github/workflows/cla.yml: -------------------------------------------------------------------------------- 1 | name: "CLA Assistant" 2 | on: 3 | issue_comment: 4 | types: [created] 5 | pull_request_target: 6 | types: [opened,closed,synchronize] 7 | 8 | jobs: 9 | CLAssistant: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: "CLA Assistant" 13 | 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' 14 | uses: cla-assistant/github-action@v2.6.1 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | # the below token should have repo scope and must be manually added by you in the repository's secret 18 | PERSONAL_ACCESS_TOKEN : ${{ secrets.PERSONAL_ACCESS_TOKEN }} 19 | with: 20 | path-to-signatures: 'signatures/version1/cla.json' 21 | path-to-document: 'https://github.com/Codeuctivity/ImageSharp.Compare/blob/main/cla.md' # e.g. a CLA or a DCO document 22 | # branch should not be protected 23 | branch: 'cla' 24 | allowlist: dependabot[bot],stesee 25 | 26 | #below are the optional inputs - If the optional inputs are not given, then default values will be taken 27 | #remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) 28 | #remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository) 29 | #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' 30 | #signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo' 31 | #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' 32 | #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' 33 | #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' 34 | #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) 35 | #use-dco-flag: true - If you are using DCO instead of CLA 36 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET build and test 2 | env: 3 | CURRENT_VERSION: 4.1.${{ github.run_number }} 4 | LAST_COMMIT_MESSAGE: ${{ github.event.head_commit.message }} 5 | 6 | on: 7 | push: 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest, windows-latest] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Setup .NET 19 | uses: actions/setup-dotnet@v4 20 | with: 21 | dotnet-version: | 22 | 8.0.x 23 | 9.0.x 24 | - name: Restore dependencies 25 | run: dotnet restore 26 | - name: Build 27 | run: dotnet build --configuration Release --no-restore 28 | - name: Test 29 | run: dotnet test --no-build --verbosity normal --configuration Release 30 | 31 | deployRelease: 32 | if: startsWith(github.ref, 'refs/heads/release') 33 | runs-on: ubuntu-latest 34 | needs: build 35 | steps: 36 | - uses: actions/checkout@v4 37 | - name: Setup .NET 38 | uses: actions/setup-dotnet@v4 39 | with: 40 | dotnet-version: | 41 | 8.0.x 42 | 9.0.x 43 | - name: Restore dependencies 44 | run: dotnet restore 45 | - name: Build 46 | run: dotnet build --configuration Release --no-restore 47 | - name: NugetPush 48 | env: 49 | NUGET_TOKEN_EXISTS: ${{ secrets.NUGET_TOKEN }} 50 | if: env.NUGET_TOKEN_EXISTS != '' 51 | run: | 52 | dotnet nuget push ./ImageSharpCompare/bin/Release/*.nupkg --skip-duplicate --api-key ${{secrets.NUGET_TOKEN}} --source https://api.nuget.org/v3/index.json 53 | - name: Github release 54 | shell: bash 55 | env: 56 | GITHUB_TOKEN: ${{ github.TOKEN }} 57 | if: env.GITHUB_TOKEN != '' 58 | run: | 59 | gh release create ${{env.CURRENT_VERSION}} ./ImageSharpCompare/bin/Release/*.*nupkg --generate-notes 60 | 61 | deployTest: 62 | if: github.ref == 'refs/heads/main' 63 | runs-on: ubuntu-latest 64 | needs: build 65 | steps: 66 | - uses: actions/checkout@v4 67 | - name: Setup .NET 68 | uses: actions/setup-dotnet@v4 69 | with: 70 | dotnet-version: | 71 | 8.0.x 72 | 9.0.x 73 | - name: Check formatting 74 | run: dotnet format --verify-no-changes 75 | - name: Restore dependencies 76 | run: dotnet restore 77 | - name: Build 78 | run: dotnet build --configuration Release --no-restore 79 | - name: NugetPush 80 | env: 81 | NUGET_TOKEN_EXISTS: ${{ secrets.NUGET_TEST_TOKEN }} 82 | if: env.NUGET_TOKEN_EXISTS != '' 83 | run: | 84 | ls ./ImageSharpCompare/bin/Release 85 | dotnet nuget push ./ImageSharpCompare/bin/Release/*.nupkg --skip-duplicate --api-key ${{secrets.NUGET_TEST_TOKEN}} --source https://apiint.nugettest.org/v3/index.json 86 | - name: Github prerelease 87 | shell: bash 88 | env: 89 | GITHUB_TOKEN: ${{ github.TOKEN }} 90 | if: env.GITHUB_TOKEN != '' 91 | run: | 92 | gh release create ${{env.CURRENT_VERSION}} ./ImageSharpCompare/bin/Release/*.*nupkg --prerelease --generate-notes 93 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '44 1 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v9 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | stale-issue-message: 'Stale issue message' 25 | stale-pr-message: 'Stale pull request message' 26 | stale-issue-label: 'no-issue-activity' 27 | stale-pr-label: 'no-pr-activity' 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "mhutchie.git-graph", 4 | "streetsidesoftware.code-spell-checker", 5 | "timonwong.shellcheck", 6 | "redhat.vscode-xml", 7 | "redhat.vscode-yaml" 8 | ] 9 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/ImageSharpCompareTestNunit/bin/Debug/net6.0/ImageSharpCompareTestNunit.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/ImageSharpCompareTestNunit", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach", 24 | "processId": "${command:pickProcess}" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Codacy", 4 | "Codeuctivity", 5 | "Nuget", 6 | "Nunit", 7 | "Rgba", 8 | "nupkg", 9 | "snupkg" 10 | ] 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/ImageSharpCompareTestNunit/ImageSharpCompareTestNunit.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/ImageSharpCompareTestNunit/ImageSharpCompareTestNunit.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/ImageSharpCompareTestNunit/ImageSharpCompareTestNunit.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | }, 41 | { 42 | "label": "update nuget packages", 43 | "type": "shell", 44 | "command": "'./.vscode/updateNuget.sh'", 45 | "problemMatcher": [] 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /.vscode/updateNuget.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | regex='PackageReference Include="([^"]*)" Version="([^"]*)"' 3 | find . -name "*.*proj" | while read -r proj; do 4 | while read -r line; do 5 | if [[ $line =~ $regex ]]; then 6 | name="${BASH_REMATCH[1]}" 7 | version="${BASH_REMATCH[2]}" 8 | if [[ $version != *-* ]]; then 9 | dotnet add "$proj" package "$name" 10 | fi 11 | fi 12 | done <"$proj" 13 | done 14 | -------------------------------------------------------------------------------- /ImageSharpCompare.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharpCompare", "ImageSharpCompare\ImageSharpCompare.csproj", "{BD710BB4-A442-449E-A277-E69218058370}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageSharpCompareTestNunit", "ImageSharpCompareTestNunit\ImageSharpCompareTestNunit.csproj", "{C8DCE47D-72ED-471E-84C6-6A3A8EAA6081}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A1B6F6C3-ECBE-471C-AE18-199DEF5982C3}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | .github\workflows\cla.yml = .github\workflows\cla.yml 14 | .github\dependabot.yml = .github\dependabot.yml 15 | .github\workflows\dotnet.yml = .github\workflows\dotnet.yml 16 | README.md = README.md 17 | .github\workflows\stale.yml = .github\workflows\stale.yml 18 | EndProjectSection 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {BD710BB4-A442-449E-A277-E69218058370}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {BD710BB4-A442-449E-A277-E69218058370}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {BD710BB4-A442-449E-A277-E69218058370}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {BD710BB4-A442-449E-A277-E69218058370}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {C8DCE47D-72ED-471E-84C6-6A3A8EAA6081}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {C8DCE47D-72ED-471E-84C6-6A3A8EAA6081}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {C8DCE47D-72ED-471E-84C6-6A3A8EAA6081}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {C8DCE47D-72ED-471E-84C6-6A3A8EAA6081}.Release|Any CPU.Build.0 = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | GlobalSection(ExtensibilityGlobals) = postSolution 39 | SolutionGuid = {AC72E02E-48E4-4DDE-B1FF-CEF86C9DFCAB} 40 | EndGlobalSection 41 | EndGlobal 42 | -------------------------------------------------------------------------------- /ImageSharpCompare/CompareResult.cs: -------------------------------------------------------------------------------- 1 | namespace Codeuctivity.ImageSharpCompare 2 | { 3 | /// 4 | /// Dto - outcome of compared images 5 | /// 6 | public class CompareResult : ICompareResult 7 | { 8 | /// 9 | /// Mean pixel error of absolute pixel error 10 | /// 11 | /// 0-765 12 | public double MeanError { get; } 13 | 14 | /// 15 | /// Absolute pixel error, counts each color channel on every pixel the delta 16 | /// 17 | public int AbsoluteError { get; } 18 | 19 | /// 20 | /// Number of pixels that differ between images 21 | /// 22 | public int PixelErrorCount { get; } 23 | 24 | /// 25 | /// Percentage of pixels that differ between images 26 | /// 27 | /// 0-100.0 28 | public double PixelErrorPercentage { get; } 29 | 30 | /// 31 | /// ctor for CompareResult 32 | /// 33 | /// Mean error 34 | /// Absolute error 35 | /// Number of pixels that differ between images 36 | /// Percentage of pixels that differ between images 37 | public CompareResult(int absoluteError, double meanError, int pixelErrorCount, double pixelErrorPercentage) 38 | { 39 | MeanError = meanError; 40 | AbsoluteError = absoluteError; 41 | PixelErrorCount = pixelErrorCount; 42 | PixelErrorPercentage = pixelErrorPercentage; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /ImageSharpCompare/ICompareResult.cs: -------------------------------------------------------------------------------- 1 | namespace Codeuctivity.ImageSharpCompare 2 | { 3 | /// 4 | /// Dto - of compared images 5 | /// 6 | public interface ICompareResult 7 | { 8 | /// 9 | /// Mean relative pixel error 10 | /// 11 | double MeanError { get; } 12 | 13 | /// 14 | /// Absolute pixel error 15 | /// 16 | int AbsoluteError { get; } 17 | 18 | /// 19 | /// Number of pixels that differ between images 20 | /// 21 | int PixelErrorCount { get; } 22 | 23 | /// 24 | /// Percentage of pixels that differ between images 25 | /// 26 | double PixelErrorPercentage { get; } 27 | } 28 | } -------------------------------------------------------------------------------- /ImageSharpCompare/ImageSharpCompare.cs: -------------------------------------------------------------------------------- 1 | using SixLabors.ImageSharp; 2 | using SixLabors.ImageSharp.PixelFormats; 3 | using SixLabors.ImageSharp.Processing; 4 | using System; 5 | using System.IO; 6 | 7 | namespace Codeuctivity.ImageSharpCompare 8 | { 9 | /// 10 | /// ImageSharpCompare, compares images. 11 | /// Use this class to compare images using a third image as mask of regions where your two compared images may differ. 12 | /// An alpha channel is ignored. 13 | /// 14 | #pragma warning disable CA1724 // Type names should not match namespaces - this is accepted for now to prevent a breaking change 15 | 16 | public static class ImageSharpCompare 17 | #pragma warning restore CA1724 // Type names should not match namespaces - this is accepted for now to prevent a breaking change 18 | { 19 | private const string sizeDiffersExceptionMessage = "Size of images differ."; 20 | 21 | private static bool ImagesHaveSameDimension(Image actual, Image expected) 22 | { 23 | ArgumentNullException.ThrowIfNull(actual); 24 | ArgumentNullException.ThrowIfNull(expected); 25 | return actual.Height == expected.Height && actual.Width == expected.Width; 26 | } 27 | 28 | private static (Image, Image) GrowToSameDimension(Image actual, Image expected) 29 | { 30 | var biggestWidth = actual.Width > expected.Width ? actual.Width : expected.Width; 31 | var biggestHeight = actual.Height > expected.Height ? actual.Height : expected.Height; 32 | 33 | var grownExpected = expected.Clone(); 34 | var grownActual = actual.Clone(); 35 | grownActual.Mutate(x => x.Resize(biggestWidth, biggestHeight)); 36 | grownExpected.Mutate(x => x.Resize(biggestWidth, biggestHeight)); 37 | 38 | return (grownActual, grownExpected); 39 | } 40 | 41 | private static (Image, Image, Image) GrowToSameDimension(Image actual, Image expected, Image mask) 42 | { 43 | var biggestWidth = actual.Width > expected.Width ? actual.Width : expected.Width; 44 | biggestWidth = biggestWidth > mask.Width ? biggestWidth : mask.Width; 45 | var biggestHeight = actual.Height > expected.Height ? actual.Height : expected.Height; 46 | biggestHeight = biggestHeight > mask.Height ? biggestHeight : mask.Height; 47 | 48 | var grownExpected = expected.Clone(); 49 | var grownActual = actual.Clone(); 50 | var grownMask = mask.Clone(); 51 | grownActual.Mutate(x => x.Resize(biggestWidth, biggestHeight)); 52 | grownExpected.Mutate(x => x.Resize(biggestWidth, biggestHeight)); 53 | grownMask.Mutate(x => x.Resize(biggestWidth, biggestHeight)); 54 | 55 | return (grownActual, grownExpected, grownMask); 56 | } 57 | 58 | /// 59 | /// Is true if width and height of both images are equal 60 | /// 61 | /// 62 | /// 63 | /// 64 | public static bool ImagesHaveEqualSize(string pathImageActual, string pathImageExpected) 65 | { 66 | using var actualImage = Image.Load(pathImageActual); 67 | using var expectedImage = Image.Load(pathImageExpected); 68 | return ImagesHaveEqualSize(actualImage, expectedImage); 69 | } 70 | 71 | /// 72 | /// Is true if width and height of both images are equal 73 | /// 74 | /// 75 | /// 76 | /// 77 | public static bool ImagesHaveEqualSize(Stream actual, Stream expected) 78 | { 79 | using var actualImage = Image.Load(actual); 80 | using var expectedImage = Image.Load(expected); 81 | return ImagesHaveEqualSize(actualImage, expectedImage); 82 | } 83 | 84 | /// 85 | /// Is true if width and height of both images are equal 86 | /// 87 | /// 88 | /// 89 | /// 90 | public static bool ImagesHaveEqualSize(Image actualImage, Image expectedImage) 91 | { 92 | return ImagesHaveSameDimension(actualImage, expectedImage); 93 | } 94 | 95 | /// 96 | /// Compares two images for equivalence 97 | /// 98 | /// 99 | /// 100 | /// 101 | /// 102 | /// True if every pixel of actual is equal to expected 103 | public static bool ImagesAreEqual(string pathImageActual, string pathImageExpected, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 104 | { 105 | using var actualImage = Image.Load(pathImageActual); 106 | using var expectedImage = Image.Load(pathImageExpected); 107 | return ImagesAreEqual(actualImage, expectedImage, resizeOption, pixelColorShiftTolerance); 108 | } 109 | 110 | /// 111 | /// Compares two images for equivalence 112 | /// 113 | /// 114 | /// 115 | /// 116 | /// True if every pixel of actual is equal to expected 117 | public static bool ImagesAreEqual(Stream actual, Stream expected, ResizeOption resizeOption = ResizeOption.DontResize) 118 | { 119 | using var actualImage = Image.Load(actual); 120 | using var expectedImage = Image.Load(expected); 121 | return ImagesAreEqual(actualImage, expectedImage, resizeOption); 122 | } 123 | 124 | /// 125 | /// Compares two images for equivalence 126 | /// 127 | /// 128 | /// 129 | /// 130 | /// 131 | /// True if every pixel of actual is equal to expected 132 | public static bool ImagesAreEqual(Image actual, Image expected, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 133 | { 134 | ArgumentNullException.ThrowIfNull(actual); 135 | 136 | ArgumentNullException.ThrowIfNull(expected); 137 | 138 | var ownsActual = false; 139 | var ownsExpected = false; 140 | Image? actualPixelAccessibleImage = null; 141 | Image? expectedPixelAccusableImage = null; 142 | try 143 | { 144 | actualPixelAccessibleImage = ImageSharpPixelTypeConverter.ToRgb24Image(actual, out ownsActual); 145 | expectedPixelAccusableImage = ImageSharpPixelTypeConverter.ToRgb24Image(expected, out ownsExpected); 146 | 147 | return ImagesAreEqual(actualPixelAccessibleImage, expectedPixelAccusableImage, resizeOption, pixelColorShiftTolerance); 148 | } 149 | finally 150 | { 151 | if (ownsActual) 152 | { 153 | actualPixelAccessibleImage?.Dispose(); 154 | } 155 | if (ownsExpected) 156 | { 157 | expectedPixelAccusableImage?.Dispose(); 158 | } 159 | } 160 | } 161 | 162 | /// 163 | /// Compares two images for equivalence 164 | /// 165 | /// 166 | /// 167 | /// 168 | /// 169 | /// True if every pixel of actual is equal to expected 170 | public static bool ImagesAreEqual(Image actual, Image expected, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 171 | { 172 | ArgumentNullException.ThrowIfNull(actual); 173 | 174 | ArgumentNullException.ThrowIfNull(expected); 175 | 176 | if (resizeOption == ResizeOption.DontResize && !ImagesHaveSameDimension(actual, expected)) 177 | { 178 | return false; 179 | } 180 | 181 | if (resizeOption == ResizeOption.DontResize || ImagesHaveSameDimension(actual, expected)) 182 | { 183 | for (var x = 0; x < actual.Width; x++) 184 | { 185 | for (var y = 0; y < actual.Height; y++) 186 | { 187 | if (pixelColorShiftTolerance == 0 && !actual[x, y].Equals(expected[x, y])) 188 | { 189 | return false; 190 | } 191 | else if (pixelColorShiftTolerance > 0) 192 | { 193 | var actualPixel = actual[x, y]; 194 | var expectedPixel = expected[x, y]; 195 | if (Math.Abs(actualPixel.R - expectedPixel.R) > pixelColorShiftTolerance || 196 | Math.Abs(actualPixel.G - expectedPixel.G) > pixelColorShiftTolerance || 197 | Math.Abs(actualPixel.B - expectedPixel.B) > pixelColorShiftTolerance) 198 | { 199 | return false; 200 | } 201 | } 202 | } 203 | } 204 | 205 | return true; 206 | } 207 | 208 | var grown = GrowToSameDimension(actual, expected); 209 | try 210 | { 211 | return ImagesAreEqual(grown.Item1, grown.Item2, ResizeOption.DontResize, pixelColorShiftTolerance); 212 | } 213 | finally 214 | { 215 | grown.Item1?.Dispose(); 216 | grown.Item2?.Dispose(); 217 | } 218 | } 219 | 220 | /// 221 | /// Calculates ICompareResult expressing the amount of difference of both images 222 | /// 223 | /// 224 | /// 225 | /// 226 | /// 227 | /// Mean and absolute pixel error 228 | public static ICompareResult CalcDiff(string pathActualImage, string pathExpectedImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 229 | { 230 | using var actual = Image.Load(pathActualImage); 231 | using var expected = Image.Load(pathExpectedImage); 232 | return CalcDiff(actual, expected, resizeOption, pixelColorShiftTolerance); 233 | } 234 | 235 | /// 236 | /// Calculates ICompareResult expressing the amount of difference of both images using a mask image for tolerated difference between the two images 237 | /// 238 | /// 239 | /// 240 | /// 241 | /// 242 | /// 243 | /// Mean and absolute pixel error 244 | public static ICompareResult CalcDiff(string pathActualImage, string pathExpectedImage, string pathMaskImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 245 | { 246 | using var actual = Image.Load(pathActualImage); 247 | using var expected = Image.Load(pathExpectedImage); 248 | using var mask = Image.Load(pathMaskImage); 249 | return CalcDiff(actual, expected, mask, resizeOption, pixelColorShiftTolerance); 250 | } 251 | 252 | /// 253 | /// Calculates ICompareResult expressing the amount of difference of both images 254 | /// 255 | /// 256 | /// 257 | /// 258 | /// 259 | /// Mean and absolute pixel error 260 | public static ICompareResult CalcDiff(Stream actualImage, Stream expectedImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 261 | { 262 | using var actual = Image.Load(actualImage); 263 | using var expected = Image.Load(expectedImage); 264 | return CalcDiff(actual, expected, resizeOption, pixelColorShiftTolerance); 265 | } 266 | 267 | /// 268 | /// Calculates ICompareResult expressing the amount of difference of both images 269 | /// 270 | /// 271 | /// 272 | /// 273 | /// 274 | /// 275 | /// 276 | public static ICompareResult CalcDiff(Stream actualImage, Stream expectedImage, Image maskImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 277 | { 278 | using var actual = Image.Load(actualImage); 279 | using var expected = Image.Load(expectedImage); 280 | return CalcDiff(actual, expected, maskImage, resizeOption, pixelColorShiftTolerance); 281 | } 282 | 283 | /// 284 | /// Calculates ICompareResult expressing the amount of difference of both images 285 | /// 286 | /// 287 | /// 288 | /// 289 | /// 290 | /// Mean and absolute pixel error 291 | public static ICompareResult CalcDiff(Image actual, Image expected, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 292 | { 293 | ArgumentNullException.ThrowIfNull(actual); 294 | ArgumentNullException.ThrowIfNull(expected); 295 | 296 | var ownsActual = false; 297 | var ownsExpected = false; 298 | Image? actualRgb24 = null; 299 | Image? expectedRgb24 = null; 300 | 301 | try 302 | { 303 | actualRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(actual, out ownsActual); 304 | expectedRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(expected, out ownsExpected); 305 | 306 | return CalcDiff(actualRgb24, expectedRgb24, resizeOption, pixelColorShiftTolerance); 307 | } 308 | finally 309 | { 310 | if (ownsActual) 311 | { 312 | actualRgb24?.Dispose(); 313 | } 314 | if (ownsExpected) 315 | { 316 | expectedRgb24?.Dispose(); 317 | } 318 | } 319 | } 320 | 321 | /// 322 | /// Calculates ICompareResult expressing the amount of difference of both images 323 | /// 324 | /// 325 | /// 326 | /// 327 | /// 328 | /// Mean and absolute pixel error 329 | public static ICompareResult CalcDiff(Image actual, Image expected, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 330 | { 331 | var imagesHaveSameDimension = ImagesHaveSameDimension(actual, expected); 332 | 333 | if (resizeOption == ResizeOption.Resize && !imagesHaveSameDimension) 334 | { 335 | var grown = GrowToSameDimension(actual, expected); 336 | try 337 | { 338 | return CalcDiff(grown.Item1, grown.Item2, ResizeOption.DontResize, pixelColorShiftTolerance); 339 | } 340 | finally 341 | { 342 | grown.Item1?.Dispose(); 343 | grown.Item2?.Dispose(); 344 | } 345 | } 346 | 347 | if (!imagesHaveSameDimension) 348 | { 349 | throw new ImageSharpCompareException(sizeDiffersExceptionMessage); 350 | } 351 | 352 | var quantity = actual.Width * actual.Height; 353 | var absoluteError = 0; 354 | var pixelErrorCount = 0; 355 | 356 | for (var x = 0; x < actual.Width; x++) 357 | { 358 | for (var y = 0; y < actual.Height; y++) 359 | { 360 | var actualPixel = actual[x, y]; 361 | var expectedPixel = expected[x, y]; 362 | 363 | var r = Math.Abs(expectedPixel.R - actualPixel.R); 364 | var g = Math.Abs(expectedPixel.G - actualPixel.G); 365 | var b = Math.Abs(expectedPixel.B - actualPixel.B); 366 | var sum = r + g + b; 367 | absoluteError += sum > pixelColorShiftTolerance ? sum : 0; 368 | pixelErrorCount += (sum > pixelColorShiftTolerance) ? 1 : 0; 369 | } 370 | } 371 | 372 | var meanError = (double)absoluteError / quantity; 373 | var pixelErrorPercentage = (double)pixelErrorCount / quantity * 100; 374 | return new CompareResult(absoluteError, meanError, pixelErrorCount, pixelErrorPercentage); 375 | } 376 | 377 | /// 378 | /// Calculates ICompareResult expressing the amount of difference of both images using a image mask for tolerated difference between the two images 379 | /// 380 | /// 381 | /// 382 | /// 383 | /// 384 | /// 385 | /// Mean and absolute pixel error 386 | public static ICompareResult CalcDiff(Image actual, Image expected, Image maskImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 387 | { 388 | ArgumentNullException.ThrowIfNull(actual); 389 | 390 | ArgumentNullException.ThrowIfNull(expected); 391 | 392 | ArgumentNullException.ThrowIfNull(maskImage); 393 | 394 | var ownsActual = false; 395 | var ownsExpected = false; 396 | var ownsMask = false; 397 | Image? actualRgb24 = null; 398 | Image? expectedRgb24 = null; 399 | Image? maskImageRgb24 = null; 400 | 401 | try 402 | { 403 | actualRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(actual, out ownsActual); 404 | expectedRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(expected, out ownsExpected); 405 | maskImageRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(maskImage, out ownsMask); 406 | 407 | return CalcDiff(actualRgb24, expectedRgb24, maskImageRgb24, resizeOption, pixelColorShiftTolerance); 408 | } 409 | finally 410 | { 411 | if (ownsActual) 412 | { 413 | actualRgb24?.Dispose(); 414 | } 415 | if (ownsExpected) 416 | { 417 | expectedRgb24?.Dispose(); 418 | } 419 | if (ownsMask) 420 | { 421 | maskImageRgb24?.Dispose(); 422 | } 423 | } 424 | } 425 | 426 | /// 427 | /// Calculates ICompareResult expressing the amount of difference of both images using a image mask for tolerated difference between the two images 428 | /// 429 | /// 430 | /// 431 | /// 432 | /// 433 | /// 434 | /// Mean and absolute pixel error 435 | public static ICompareResult CalcDiff(Image actual, Image expected, Image maskImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 436 | { 437 | ArgumentNullException.ThrowIfNull(maskImage); 438 | 439 | var imagesHaveSameDimension = ImagesHaveSameDimension(actual, expected) && ImagesHaveSameDimension(actual, maskImage); 440 | 441 | if (resizeOption == ResizeOption.Resize && !imagesHaveSameDimension) 442 | { 443 | var grown = GrowToSameDimension(actual, expected, maskImage); 444 | try 445 | { 446 | return CalcDiff(grown.Item1, grown.Item2, grown.Item3, ResizeOption.DontResize, pixelColorShiftTolerance); 447 | } 448 | finally 449 | { 450 | grown.Item1?.Dispose(); 451 | grown.Item2?.Dispose(); 452 | grown.Item3?.Dispose(); 453 | } 454 | } 455 | 456 | if (!imagesHaveSameDimension) 457 | { 458 | throw new ImageSharpCompareException(sizeDiffersExceptionMessage); 459 | } 460 | 461 | var quantity = actual.Width * actual.Height; 462 | var absoluteError = 0; 463 | var pixelErrorCount = 0; 464 | 465 | for (var x = 0; x < actual.Width; x++) 466 | { 467 | for (var y = 0; y < actual.Height; y++) 468 | { 469 | var maskImagePixel = maskImage[x, y]; 470 | var actualPixel = actual[x, y]; 471 | var expectedPixel = expected[x, y]; 472 | 473 | var r = Math.Abs(expectedPixel.R - actualPixel.R); 474 | var g = Math.Abs(expectedPixel.G - actualPixel.G); 475 | var b = Math.Abs(expectedPixel.B - actualPixel.B); 476 | 477 | var error = 0; 478 | 479 | if (r > maskImagePixel.R) 480 | { 481 | error += r; 482 | } 483 | 484 | if (g > maskImagePixel.G) 485 | { 486 | error += g; 487 | } 488 | 489 | if (b > maskImagePixel.B) 490 | { 491 | error += b; 492 | } 493 | 494 | absoluteError += error > pixelColorShiftTolerance ? error : 0; 495 | pixelErrorCount += error > pixelColorShiftTolerance ? 1 : 0; 496 | } 497 | } 498 | var meanError = (double)absoluteError / quantity; 499 | var pixelErrorPercentage = (double)pixelErrorCount / quantity * 100; 500 | return new CompareResult(absoluteError, meanError, pixelErrorCount, pixelErrorPercentage); 501 | } 502 | 503 | /// 504 | /// Creates a diff mask image of two images 505 | /// 506 | /// 507 | /// 508 | /// 509 | /// 510 | /// Image representing diff, black means no diff between actual image and expected image, white means max diff 511 | public static Image CalcDiffMaskImage(string pathActualImage, string pathExpectedImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 512 | { 513 | using var actual = Image.Load(pathActualImage); 514 | using var expected = Image.Load(pathExpectedImage); 515 | return CalcDiffMaskImage(actual, expected, resizeOption, pixelColorShiftTolerance); 516 | } 517 | 518 | /// 519 | /// Creates a diff mask image of two images 520 | /// 521 | /// 522 | /// 523 | /// 524 | /// 525 | /// 526 | /// Image representing diff, black means no diff between actual image and expected image, white means max diff 527 | public static Image CalcDiffMaskImage(string pathActualImage, string pathExpectedImage, string pathMaskImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 528 | { 529 | using var actual = Image.Load(pathActualImage); 530 | using var expected = Image.Load(pathExpectedImage); 531 | using var mask = Image.Load(pathMaskImage); 532 | return CalcDiffMaskImage(actual, expected, mask, resizeOption, pixelColorShiftTolerance); 533 | } 534 | 535 | /// 536 | /// Creates a diff mask image of two images 537 | /// 538 | /// 539 | /// 540 | /// 541 | /// 542 | /// Image representing diff, black means no diff between actual image and expected image, white means max diff 543 | public static Image CalcDiffMaskImage(Stream actualImage, Stream expectedImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 544 | { 545 | using var actual = Image.Load(actualImage); 546 | using var expected = Image.Load(expectedImage); 547 | return CalcDiffMaskImage(actual, expected, resizeOption, pixelColorShiftTolerance); 548 | } 549 | 550 | /// 551 | /// Creates a diff mask image of two images 552 | /// 553 | /// 554 | /// 555 | /// 556 | /// 557 | /// 558 | /// Image representing diff, black means no diff between actual image and expected image, white means max diff 559 | public static Image CalcDiffMaskImage(Stream actualImage, Stream expectedImage, Stream maskImage, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 560 | { 561 | using var actual = Image.Load(actualImage); 562 | using var expected = Image.Load(expectedImage); 563 | using var mask = Image.Load(maskImage); 564 | return CalcDiffMaskImage(actual, expected, mask, resizeOption, pixelColorShiftTolerance); 565 | } 566 | 567 | /// 568 | /// Creates a diff mask image of two images 569 | /// 570 | /// 571 | /// 572 | /// 573 | /// 574 | /// Image representing diff, black means no diff between actual image and expected image, white means max diff 575 | public static Image CalcDiffMaskImage(Image actual, Image expected, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 576 | { 577 | ArgumentNullException.ThrowIfNull(actual); 578 | 579 | ArgumentNullException.ThrowIfNull(expected); 580 | 581 | var ownsActual = false; 582 | var ownsExpected = false; 583 | Image? actualRgb24 = null; 584 | Image? expectedRgb24 = null; 585 | 586 | try 587 | { 588 | actualRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(actual, out ownsActual); 589 | expectedRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(expected, out ownsExpected); 590 | 591 | return CalcDiffMaskImage(actualRgb24, expectedRgb24, resizeOption, pixelColorShiftTolerance); 592 | } 593 | finally 594 | { 595 | if (ownsActual) 596 | { 597 | actualRgb24?.Dispose(); 598 | } 599 | if (ownsExpected) 600 | { 601 | expectedRgb24?.Dispose(); 602 | } 603 | } 604 | } 605 | 606 | /// 607 | /// Creates a diff mask image of two images 608 | /// 609 | /// 610 | /// 611 | /// 612 | /// 613 | /// 614 | /// Image representing diff, black means no diff between actual image and expected image, white means max diff 615 | public static Image CalcDiffMaskImage(Image actual, Image expected, Image mask, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 616 | { 617 | ArgumentNullException.ThrowIfNull(actual); 618 | ArgumentNullException.ThrowIfNull(expected); 619 | ArgumentNullException.ThrowIfNull(mask); 620 | var ownsActual = false; 621 | var ownsExpected = false; 622 | var ownsMask = false; 623 | Image? actualRgb24 = null; 624 | Image? expectedRgb24 = null; 625 | Image? maskRgb24 = null; 626 | 627 | try 628 | { 629 | actualRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(actual, out ownsActual); 630 | expectedRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(expected, out ownsExpected); 631 | maskRgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(mask, out ownsMask); 632 | 633 | return CalcDiffMaskImage(actualRgb24, expectedRgb24, maskRgb24, resizeOption, pixelColorShiftTolerance); 634 | } 635 | finally 636 | { 637 | if (ownsActual) 638 | { 639 | actualRgb24?.Dispose(); 640 | } 641 | if (ownsExpected) 642 | { 643 | expectedRgb24?.Dispose(); 644 | } 645 | if (ownsMask) 646 | { 647 | maskRgb24?.Dispose(); 648 | } 649 | } 650 | } 651 | 652 | /// 653 | /// Creates a diff mask image of two images 654 | /// 655 | /// 656 | /// 657 | /// 658 | /// 659 | /// Image representing diff, black means no diff between actual image and expected image, white means max diff 660 | public static Image CalcDiffMaskImage(Image actual, Image expected, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 661 | { 662 | var imagesHAveSameDimension = ImagesHaveSameDimension(actual, expected); 663 | 664 | if (resizeOption == ResizeOption.DontResize && !imagesHAveSameDimension) 665 | { 666 | throw new ImageSharpCompareException(sizeDiffersExceptionMessage); 667 | } 668 | 669 | if (imagesHAveSameDimension) 670 | { 671 | var maskImage = new Image(actual.Width, actual.Height); 672 | 673 | for (var x = 0; x < actual.Width; x++) 674 | { 675 | for (var y = 0; y < actual.Height; y++) 676 | { 677 | var actualPixel = actual[x, y]; 678 | var expectedPixel = expected[x, y]; 679 | 680 | if (pixelColorShiftTolerance == 0) 681 | { 682 | var pixel = new Rgb24 683 | { 684 | R = (byte)Math.Abs(actualPixel.R - expectedPixel.R), 685 | G = (byte)Math.Abs(actualPixel.G - expectedPixel.G), 686 | B = (byte)Math.Abs(actualPixel.B - expectedPixel.B) 687 | }; 688 | 689 | maskImage[x, y] = pixel; 690 | } 691 | else 692 | { 693 | var r = Math.Abs(actualPixel.R - expectedPixel.R); 694 | var g = Math.Abs(actualPixel.G - expectedPixel.G); 695 | var b = Math.Abs(actualPixel.B - expectedPixel.B); 696 | 697 | var error = r + g + b; 698 | if (error <= pixelColorShiftTolerance) 699 | { 700 | r = 0; 701 | g = 0; 702 | b = 0; 703 | } 704 | 705 | var pixel = new Rgb24 706 | { 707 | R = (byte)r, 708 | G = (byte)g, 709 | B = (byte)b 710 | }; 711 | 712 | maskImage[x, y] = pixel; 713 | } 714 | } 715 | } 716 | return maskImage; 717 | } 718 | 719 | var grown = GrowToSameDimension(actual, expected); 720 | try 721 | { 722 | return CalcDiffMaskImage(grown.Item1, grown.Item2, ResizeOption.DontResize, pixelColorShiftTolerance); 723 | } 724 | finally 725 | { 726 | grown.Item1?.Dispose(); 727 | grown.Item2?.Dispose(); 728 | } 729 | } 730 | 731 | /// 732 | /// Creates a diff mask image of two images using a image mask for tolerated difference between the two images. 733 | /// 734 | /// 735 | /// 736 | /// 737 | /// 738 | /// 739 | /// Image representing diff, black means no diff between actual image and expected image, white means max diff 740 | public static Image CalcDiffMaskImage(Image actual, Image expected, Image mask, ResizeOption resizeOption = ResizeOption.DontResize, int pixelColorShiftTolerance = 0) 741 | { 742 | ArgumentNullException.ThrowIfNull(mask); 743 | var imagesHaveSameDimensions = ImagesHaveSameDimension(actual, expected) && ImagesHaveSameDimension(actual, mask); 744 | 745 | if (!imagesHaveSameDimensions && resizeOption == ResizeOption.DontResize) 746 | { 747 | throw new ImageSharpCompareException(sizeDiffersExceptionMessage); 748 | } 749 | 750 | if (imagesHaveSameDimensions) 751 | { 752 | var maskImageResult = new Image(actual.Width, actual.Height); 753 | 754 | for (var x = 0; x < actual.Width; x++) 755 | { 756 | for (var y = 0; y < actual.Height; y++) 757 | { 758 | var maskPixel = mask[x, y]; 759 | var actualPixel = actual[x, y]; 760 | var expectedPixel = expected[x, y]; 761 | 762 | maskImageResult[x, y] = new Rgb24 763 | { 764 | R = (byte)Math.Max(byte.MinValue, Math.Abs(expectedPixel.R - actualPixel.R) - maskPixel.R), 765 | G = (byte)Math.Max(byte.MinValue, Math.Abs(expectedPixel.G - actualPixel.G) - maskPixel.G), 766 | B = (byte)Math.Max(byte.MinValue, Math.Abs(expectedPixel.B - actualPixel.B) - maskPixel.B) 767 | }; 768 | } 769 | } 770 | 771 | return maskImageResult; 772 | } 773 | 774 | var grown = GrowToSameDimension(actual, expected, mask); 775 | try 776 | { 777 | return CalcDiffMaskImage(grown.Item1, grown.Item2, grown.Item3, ResizeOption.DontResize, pixelColorShiftTolerance); 778 | } 779 | finally 780 | { 781 | grown.Item1?.Dispose(); 782 | grown.Item2?.Dispose(); 783 | grown.Item3?.Dispose(); 784 | } 785 | } 786 | 787 | /// 788 | /// Converts a Rgba32 Image to Rgb24 one 789 | /// 790 | /// 791 | #pragma warning disable S1133 // Give the consumer of the public method some time to migrate 792 | [Obsolete("use 'imageRgba32..CloneAs()' instead")] 793 | #pragma warning restore S1133 794 | public static Image ConvertRgba32ToRgb24(Image imageRgba32) 795 | { 796 | ArgumentNullException.ThrowIfNull(imageRgba32); 797 | 798 | var maskRgb24 = new Image(imageRgba32.Width, imageRgba32.Height); 799 | 800 | for (var x = 0; x < imageRgba32.Width; x++) 801 | { 802 | for (var y = 0; y < imageRgba32.Height; y++) 803 | { 804 | var pixel = new Rgb24(); 805 | pixel.FromRgba32(imageRgba32[x, y]); 806 | 807 | maskRgb24[x, y] = pixel; 808 | } 809 | } 810 | return maskRgb24; 811 | } 812 | } 813 | } -------------------------------------------------------------------------------- /ImageSharpCompare/ImageSharpCompare.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | true 5 | true 6 | https://github.com/Codeuctivity/ImageSharp.Compare 7 | Compare Image JPG PNG BMP Linux Windows MacOs 8 | Stefan Seeland 9 | Codeuctivity 10 | $(CURRENT_VERSION) 11 | 0.0.1 12 | $(Version) 13 | $(Version) 14 | $(Version) 15 | $(LAST_COMMIT_MESSAGE) 16 | NugetIcon.png 17 | https://github.com/Codeuctivity/ImageSharp.Compare 18 | 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. 19 | Apache-2.0 20 | ImageSharpCompare.snk 21 | true 22 | true 23 | snupkg 24 | true 25 | true 26 | enable 27 | true 28 | AllEnabledByDefault 29 | latest 30 | false 31 | Codeuctivity.ImageSharpCompare 32 | nugetReadme.md 33 | Codeuctivity.ImageSharpCompare 34 | Codeuctivity.ImageSharpCompare 35 | Codeuctivity.ImageSharpCompare 36 | true 37 | 38 | 39 | True 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | all 50 | runtime; build; native; contentfiles; analyzers; buildtransitive 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /ImageSharpCompare/ImageSharpCompare.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.5.2.0 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharpCompare", "ImageSharpCompare.csproj", "{6D218566-E4FA-E901-D111-08AEB4065B5C}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {6D218566-E4FA-E901-D111-08AEB4065B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {6D218566-E4FA-E901-D111-08AEB4065B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {6D218566-E4FA-E901-D111-08AEB4065B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {6D218566-E4FA-E901-D111-08AEB4065B5C}.Release|Any CPU.Build.0 = Release|Any CPU 17 | EndGlobalSection 18 | GlobalSection(SolutionProperties) = preSolution 19 | HideSolutionNode = FALSE 20 | EndGlobalSection 21 | GlobalSection(ExtensibilityGlobals) = postSolution 22 | SolutionGuid = {239FA1C2-CD5C-4D95-BE3C-97B7B1BF4F3C} 23 | EndGlobalSection 24 | EndGlobal 25 | -------------------------------------------------------------------------------- /ImageSharpCompare/ImageSharpCompare.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompare/ImageSharpCompare.snk -------------------------------------------------------------------------------- /ImageSharpCompare/ImageSharpCompareException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Codeuctivity.ImageSharpCompare 4 | { 5 | /// 6 | /// ImageSharpCompareException gets thrown if comparing of images fails 7 | /// 8 | public class ImageSharpCompareException : Exception 9 | { 10 | /// 11 | /// ImageSharpCompareException gets thrown if comparing of images fails 12 | /// 13 | public ImageSharpCompareException() 14 | { 15 | } 16 | 17 | /// 18 | /// ImageSharpCompareException gets thrown if comparing of images fails 19 | /// 20 | /// 21 | /// 22 | public ImageSharpCompareException(string message) : base(message) 23 | { 24 | } 25 | 26 | /// 27 | /// ImageSharpCompareException gets thrown if comparing of images fails 28 | /// 29 | /// 30 | /// 31 | /// 32 | public ImageSharpCompareException(string message, Exception innerException) : base(message, innerException) 33 | { 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /ImageSharpCompare/ImageSharpPixelTypeConverter.cs: -------------------------------------------------------------------------------- 1 | using SixLabors.ImageSharp; 2 | using SixLabors.ImageSharp.PixelFormats; 3 | using System; 4 | 5 | namespace Codeuctivity.ImageSharpCompare 6 | { 7 | /// 8 | /// Provides functionality to convert an ImageSharp image of any pixel type to an ImageSharp image with Rgb24 pixel type. 9 | /// 10 | public static class ImageSharpPixelTypeConverter 11 | { 12 | /// 13 | /// Converts an Image with any pixel type to Rgb24 14 | /// 15 | /// 16 | /// Use this to dispose cloned instances 17 | public static Image ToRgb24Image(Image image, out bool isClonedInstance) 18 | { 19 | ArgumentNullException.ThrowIfNull(image); 20 | 21 | if (image is Image actualPixelAccessibleImage) 22 | { 23 | isClonedInstance = false; 24 | return actualPixelAccessibleImage; 25 | } 26 | 27 | isClonedInstance = true; 28 | return image.CloneAs(); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /ImageSharpCompare/NugetIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompare/NugetIcon.png -------------------------------------------------------------------------------- /ImageSharpCompare/ResizeOption.cs: -------------------------------------------------------------------------------- 1 | namespace Codeuctivity.ImageSharpCompare 2 | { 3 | /// 4 | /// Options that are applied if images do have different image dimension 5 | /// 6 | public enum ResizeOption 7 | { 8 | /// 9 | /// Dont resize images with different size. 10 | /// 11 | DontResize, 12 | 13 | /// 14 | /// Images with different size will get resized before pixel based compare is used to determine equality. 15 | /// 16 | Resize 17 | } 18 | } -------------------------------------------------------------------------------- /ImageSharpCompare/docs/nugetReadme.md: -------------------------------------------------------------------------------- 1 | 2 | Compares images 3 | Supports comparing images by using a tolerance mask image. 4 | Linux, Windows, MacOs supported. 5 | 6 | Basic example: 7 | 8 | ```csharp 9 | bool isEqual = ImageSharpCompare.ImagesAreEqual("actual.png", "expected.png"); 10 | 11 | // calcs MeanError, AbsoluteError, PixelErrorCount and PixelErrorPercentage 12 | ICompareResult calcDiff = ImageSharpCompare.CalcDiff("actual.png", "expected.png"); 13 | ``` 14 | -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/AssertDisposeBehavior.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using SixLabors.ImageSharp; 3 | using System; 4 | using System.Reflection; 5 | 6 | namespace ImageSharpCompareTestNunit 7 | { 8 | internal static class AssertDisposeBehavior 9 | { 10 | 11 | internal static void AssertThatImageIsDisposed(Image image, bool expectedDisposeState = false) 12 | { 13 | const string imageSharpPrivateFieldNameIsDisposed = "isDisposed"; 14 | var isDisposed = (bool?)GetInstanceField(image, imageSharpPrivateFieldNameIsDisposed); 15 | Assert.That(isDisposed, Is.EqualTo(expectedDisposeState)); 16 | image.Dispose(); 17 | isDisposed = (bool?)GetInstanceField(image, imageSharpPrivateFieldNameIsDisposed); 18 | Assert.That(isDisposed, Is.True); 19 | } 20 | 21 | private static object? GetInstanceField(T instance, string fieldName) 22 | { 23 | var bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static; 24 | var field = typeof(T).GetField(fieldName, bindFlags); 25 | return field == null ? throw new ArgumentNullException(fieldName) : field.GetValue(instance); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/ImageSharpCompareTest.cs: -------------------------------------------------------------------------------- 1 | using Codeuctivity.ImageSharpCompare; 2 | using NUnit.Framework; 3 | using SixLabors.ImageSharp; 4 | using SixLabors.ImageSharp.PixelFormats; 5 | using System; 6 | using System.IO; 7 | 8 | namespace ImageSharpCompareTestNunit 9 | { 10 | public class IntegrationTest 11 | { 12 | private const string jpg0Rgb24 = "../../../TestData/Calc0.jpg"; 13 | private const string jpg1Rgb24 = "../../../TestData/Calc1.jpg"; 14 | private const string png0Rgba32 = "../../../TestData/Calc0.png"; 15 | private const string png1Rgba32 = "../../../TestData/Calc1.png"; 16 | private const string pngBlack2x2px = "../../../TestData/Black.png"; 17 | private const string pngBlack4x4px = "../../../TestData/BlackDoubleSize.png"; 18 | private const string pngWhite2x2px = "../../../TestData/White.png"; 19 | private const string pngTransparent2x2px = "../../../TestData/pngTransparent2x2px.png"; 20 | private const string renderedForm1 = "../../../TestData/HC007-Test-02-3-OxPt.html1.png"; 21 | private const string renderedForm2 = "../../../TestData/HC007-Test-02-3-OxPt.html2.png"; 22 | private const string colorShift1 = "../../../TestData/ColorShift1.png"; 23 | private const string colorShift2 = "../../../TestData/ColorShift2.png"; 24 | 25 | [Test] 26 | [TestCase(jpg0Rgb24, jpg0Rgb24, true)] 27 | [TestCase(png0Rgba32, png0Rgba32, true)] 28 | [TestCase(png0Rgba32, jpg0Rgb24, true)] 29 | [TestCase(png0Rgba32, jpg1Rgb24, true)] 30 | [TestCase(png0Rgba32, pngBlack2x2px, false)] 31 | public void ShouldVerifyThatImagesFromFilePathSizeAreEqual(string pathActual, string pathExpected, bool expectedOutcome) 32 | { 33 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual); 34 | var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected); 35 | 36 | Assert.That(ImageSharpCompare.ImagesHaveEqualSize(absolutePathActual, absolutePathExpected), Is.EqualTo(expectedOutcome)); 37 | } 38 | 39 | [Test] 40 | [TestCase(jpg0Rgb24, jpg0Rgb24, true)] 41 | [TestCase(png0Rgba32, png0Rgba32, true)] 42 | [TestCase(png0Rgba32, jpg0Rgb24, true)] 43 | [TestCase(png0Rgba32, jpg1Rgb24, true)] 44 | [TestCase(png0Rgba32, pngBlack2x2px, false)] 45 | public void ShouldVerifyThatImagesSizeAreEqual(string pathActual, string pathExpected, bool expectedOutcome) 46 | { 47 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual); 48 | var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected); 49 | 50 | using var actual = Image.Load(absolutePathActual); 51 | using var expected = Image.Load(absolutePathExpected); 52 | 53 | Assert.That(ImageSharpCompare.ImagesHaveEqualSize(absolutePathActual, absolutePathExpected), Is.EqualTo(expectedOutcome)); 54 | } 55 | 56 | [Test] 57 | [TestCase(jpg0Rgb24, jpg0Rgb24, true)] 58 | [TestCase(png0Rgba32, png0Rgba32, true)] 59 | [TestCase(png0Rgba32, jpg0Rgb24, true)] 60 | [TestCase(png0Rgba32, jpg1Rgb24, true)] 61 | [TestCase(png0Rgba32, pngBlack2x2px, false)] 62 | public void ShouldVerifyThatImageStreamsSizeAreEqual(string pathActual, string pathExpected, bool expectedOutcome) 63 | { 64 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual); 65 | var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected); 66 | 67 | using var actual = new FileStream(absolutePathActual, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 68 | using var expected = new FileStream(absolutePathExpected, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 69 | 70 | Assert.That(ImageSharpCompare.ImagesHaveEqualSize(absolutePathActual, absolutePathExpected), Is.EqualTo(expectedOutcome)); 71 | } 72 | 73 | [Test] 74 | [TestCase(jpg0Rgb24, jpg0Rgb24)] 75 | [TestCase(png0Rgba32, png0Rgba32)] 76 | public void ShouldVerifyThatImagesAreEqual(string pathActual, string pathExpected) 77 | { 78 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual); 79 | var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected); 80 | 81 | Assert.That(ImageSharpCompare.ImagesAreEqual(absolutePathActual, absolutePathExpected), Is.True); 82 | } 83 | 84 | [Test] 85 | [TestCase(pngBlack2x2px, pngBlack2x2px, ResizeOption.Resize, true)] 86 | [TestCase(pngBlack2x2px, pngBlack4x4px, ResizeOption.Resize, true)] 87 | [TestCase(pngBlack2x2px, pngBlack4x4px, ResizeOption.DontResize, false)] 88 | [TestCase(colorShift1, colorShift2, ResizeOption.DontResize, false)] 89 | public void ShouldVerifyThatImagesWithDifferentSizeAreEqual(string pathActual, string pathExpected, ResizeOption resizeOption, bool expectedResult) 90 | { 91 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual); 92 | var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected); 93 | 94 | Assert.That(ImageSharpCompare.ImagesAreEqual(absolutePathActual, absolutePathExpected, resizeOption), Is.EqualTo(expectedResult)); 95 | } 96 | [Test] 97 | [TestCase(colorShift1, colorShift2, ResizeOption.DontResize, 0, false)] 98 | [TestCase(colorShift1, colorShift2, ResizeOption.Resize, 0, false)] 99 | [TestCase(colorShift1, colorShift2, ResizeOption.DontResize, 15, true)] 100 | [TestCase(colorShift1, colorShift2, ResizeOption.Resize, 15, true)] 101 | public void ShouldVerifyThatImagesWithColorShift(string pathActual, string pathExpected, ResizeOption resizeOption, int expectedColorShift, bool expectedResult) 102 | { 103 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual); 104 | var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected); 105 | 106 | Assert.That(ImageSharpCompare.ImagesAreEqual(absolutePathActual, absolutePathExpected, resizeOption, expectedColorShift), Is.EqualTo(expectedResult)); 107 | } 108 | 109 | [Test] 110 | [TestCase(jpg0Rgb24, jpg0Rgb24)] 111 | [TestCase(png0Rgba32, png0Rgba32)] 112 | public void ShouldVerifyThatImageStreamsAreEqual(string pathActual, string pathExpected) 113 | { 114 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual); 115 | var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected); 116 | 117 | using var actual = new FileStream(absolutePathActual, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 118 | using var expected = new FileStream(absolutePathExpected, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 119 | 120 | Assert.That(ImageSharpCompare.ImagesAreEqual(actual, expected), Is.True); 121 | } 122 | 123 | [Test] 124 | [TestCase(jpg0Rgb24, jpg0Rgb24)] 125 | [TestCase(png0Rgba32, png0Rgba32)] 126 | public void ShouldVerifyThatImageSharpImagesAreEqual(string pathActual, string pathExpected) 127 | { 128 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual); 129 | var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected); 130 | 131 | using var actual = Image.Load(absolutePathActual); 132 | using var expected = Image.Load(absolutePathExpected); 133 | 134 | Assert.That(ImageSharpCompare.ImagesAreEqual(actual, expected), Is.True); 135 | AssertDisposeBehavior.AssertThatImageIsDisposed(actual); 136 | AssertDisposeBehavior.AssertThatImageIsDisposed(expected); 137 | } 138 | 139 | [Test] 140 | [TestCase(jpg0Rgb24, jpg0Rgb24)] 141 | [TestCase(png0Rgba32, png0Rgba32)] 142 | public void ShouldVerifyThatImageSharpImagesAreEqualBrga(string pathActual, string pathExpected) 143 | { 144 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual); 145 | var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected); 146 | 147 | using var actual = Image.Load(absolutePathActual); 148 | using var expected = Image.Load(absolutePathExpected); 149 | 150 | Assert.That(ImageSharpCompare.ImagesAreEqual(actual, expected), Is.True); 151 | AssertDisposeBehavior.AssertThatImageIsDisposed(actual); 152 | AssertDisposeBehavior.AssertThatImageIsDisposed(expected); 153 | } 154 | 155 | [Test] 156 | [TestCase(jpg0Rgb24, jpg0Rgb24)] 157 | [TestCase(png0Rgba32, png0Rgba32)] 158 | public void ShouldVerifyThatImageSharpImagesAreEqualBgra5551(string pathActual, string pathExpected) 159 | { 160 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual); 161 | var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected); 162 | 163 | using var actual = Image.Load(absolutePathActual); 164 | using var expected = Image.Load(absolutePathExpected); 165 | 166 | Assert.That(ImageSharpCompare.ImagesAreEqual(actual, expected), Is.True); 167 | AssertDisposeBehavior.AssertThatImageIsDisposed(actual); 168 | AssertDisposeBehavior.AssertThatImageIsDisposed(expected); 169 | } 170 | 171 | [Test] 172 | [TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, ResizeOption.DontResize, 0)] 173 | [TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, ResizeOption.Resize, 0)] 174 | [TestCase(jpg1Rgb24, png1Rgba32, 382669, 2.3673566603152607d, 140893, 87.162530004206772d, ResizeOption.DontResize, 0)] 175 | [TestCase(png1Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize, 0)] 176 | [TestCase(jpg1Rgb24, jpg1Rgb24, 0, 0, 0, 0, ResizeOption.DontResize, 0)] 177 | [TestCase(jpg0Rgb24, jpg1Rgb24, 208832, 1.2919254658385093d, 2089, 1.2923461433768035d, ResizeOption.DontResize, 0)] 178 | [TestCase(png0Rgba32, png1Rgba32, 203027, 1.25601321422385d, 681, 0.42129618173269651d, ResizeOption.DontResize, 0)] 179 | [TestCase(pngBlack2x2px, pngWhite2x2px, 3060, 765, 4, 100.0d, ResizeOption.DontResize, 0)] 180 | [TestCase(pngBlack2x2px, pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize, 0)] 181 | [TestCase(pngBlack4x4px, pngWhite2x2px, 12240, 765, 16, 100.0d, ResizeOption.Resize, 0)] 182 | [TestCase(renderedForm1, renderedForm2, 50103469, 61.825603405725566d, 220164, 27.167324777887465d, ResizeOption.Resize, 0)] 183 | [TestCase(renderedForm2, renderedForm1, 50103469, 61.825603405725566d, 220164, 27.167324777887465d, ResizeOption.Resize, 0)] 184 | [TestCase(colorShift1, colorShift2, 117896, 3.437201166180758d, 30398, 88.623906705539355d, ResizeOption.DontResize, 0)] 185 | [TestCase(colorShift1, colorShift2, 0, 0, 0, 0, ResizeOption.DontResize, 15)] 186 | public void ShouldVerifyThatImagesAreSemiEqual(string pathPic1, string pathPic2, int expectedAbsoluteError, double expectedMeanError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption, int pixelColorShiftTolerance) 187 | { 188 | var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1); 189 | var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2); 190 | 191 | var diff = ImageSharpCompare.CalcDiff(absolutePathPic1, absolutePathPic2, resizeOption, pixelColorShiftTolerance); 192 | 193 | Console.WriteLine($"PixelErrorCount: {diff.PixelErrorCount}"); 194 | Console.WriteLine($"PixelErrorPercentage: {diff.PixelErrorPercentage}"); 195 | Console.WriteLine($"AbsoluteError: {diff.AbsoluteError}"); 196 | Console.WriteLine($"MeanError: {diff.MeanError}"); 197 | 198 | Assert.That(diff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError"); 199 | Assert.That(diff.MeanError, Is.EqualTo(expectedMeanError), "MeanError"); 200 | Assert.That(diff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount"); 201 | Assert.That(diff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage"); 202 | } 203 | 204 | [TestCase(pngBlack2x2px, pngBlack4x4px)] 205 | [TestCase(pngBlack4x4px, pngWhite2x2px)] 206 | public void ShouldVerifyThatCalcDiffThrowsOnDifferentImageSizes(string pathPic1, string pathPic2) 207 | { 208 | var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1); 209 | var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2); 210 | 211 | var exception = Assert.Throws( 212 | () => ImageSharpCompare.CalcDiff(absolutePathPic1, absolutePathPic2, ResizeOption.DontResize)); 213 | 214 | Assert.That(exception?.Message, Is.EqualTo("Size of images differ.")); 215 | } 216 | 217 | [Test] 218 | [TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, ResizeOption.DontResize)] 219 | [TestCase(jpg0Rgb24, png0Rgba32, 384538, 2.3789191061839596d, 140855, 87.139021553537404d, ResizeOption.Resize)] 220 | [TestCase(jpg1Rgb24, png1Rgba32, 382669, 2.3673566603152607d, 140893, 87.162530004206772d, ResizeOption.DontResize)] 221 | [TestCase(png1Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize)] 222 | [TestCase(jpg1Rgb24, jpg1Rgb24, 0, 0, 0, 0, ResizeOption.DontResize)] 223 | [TestCase(jpg0Rgb24, jpg1Rgb24, 208832, 1.2919254658385093d, 2089, 1.2923461433768035d, ResizeOption.DontResize)] 224 | [TestCase(png0Rgba32, png1Rgba32, 203027, 1.25601321422385d, 681, 0.42129618173269651d, ResizeOption.DontResize)] 225 | [TestCase(pngBlack2x2px, pngWhite2x2px, 3060, 765, 4, 100.0d, ResizeOption.DontResize)] 226 | [TestCase(pngBlack2x2px, pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize)] 227 | [TestCase(pngBlack4x4px, pngWhite2x2px, 12240, 765, 16, 100.0d, ResizeOption.Resize)] 228 | public void ShouldVerifyThatImageStreamsAreSemiEqual(string pathPic1, string pathPic2, int expectedAbsoluteError, double expectedMeanError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption) 229 | { 230 | var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1); 231 | var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2); 232 | 233 | using var pic1 = new FileStream(absolutePathPic1, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 234 | using var pic2 = new FileStream(absolutePathPic2, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 235 | 236 | var diff = ImageSharpCompare.CalcDiff(pic1, pic2, resizeOption); 237 | Assert.That(diff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError"); 238 | Assert.That(diff.MeanError, Is.EqualTo(expectedMeanError), "MeanError"); 239 | Assert.That(diff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount"); 240 | Assert.That(diff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage"); 241 | } 242 | 243 | [TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize, 0, false)] 244 | [TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.Resize, 0, false)] 245 | [TestCase(pngWhite2x2px, pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize, 0, false)] 246 | [TestCase(pngBlack4x4px, pngWhite2x2px, 0, 0, 0, 0, ResizeOption.Resize, 0, false)] 247 | [TestCase(renderedForm1, renderedForm2, 0, 0, 0, 0, ResizeOption.Resize, 0, false)] 248 | [TestCase(renderedForm2, renderedForm1, 0, 0, 0, 0, ResizeOption.Resize, 0, false)] 249 | [TestCase(colorShift1, colorShift1, 0, 0, 0, 0, ResizeOption.DontResize, 15, true)] 250 | [TestCase(colorShift1, colorShift1, 0, 0, 0, 0, ResizeOption.Resize, 15, true)] 251 | [TestCase(colorShift1, colorShift2, 0, 0, 0, 0, ResizeOption.Resize, 15, true)] 252 | [TestCase(colorShift1, colorShift2, 0, 0, 0, 0, ResizeOption.DontResize, 15, true)] 253 | [TestCase(colorShift1, colorShift2, 0, 0, 0, 0, ResizeOption.Resize, 14, false)] 254 | [TestCase(colorShift1, colorShift2, 0, 0, 0, 0, ResizeOption.DontResize, 14, false)] 255 | public void CalcDiffMaskImage(string pathPic1, string pathPic2, double expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption, int expectedColorShift, bool expectMaskToBeBlack) 256 | { 257 | var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1); 258 | var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2); 259 | var differenceMask = Path.GetTempFileName() + "differenceMask.png"; 260 | 261 | using (var fileStreamDifferenceMask = File.Create(differenceMask)) 262 | using (var maskImage = ImageSharpCompare.CalcDiffMaskImage(absolutePathPic1, absolutePathPic2, resizeOption, expectedColorShift)) 263 | { 264 | ImageExtensions.SaveAsPng(maskImage, fileStreamDifferenceMask); 265 | Assert.That(IsImageEntirelyBlack(maskImage), Is.EqualTo(expectMaskToBeBlack)); 266 | } 267 | 268 | var maskedDiff = ImageSharpCompare.CalcDiff(absolutePathPic1, absolutePathPic2, differenceMask, resizeOption, expectedColorShift); 269 | File.Delete(differenceMask); 270 | 271 | Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError"); 272 | Assert.That(maskedDiff.MeanError, Is.EqualTo(expectedMeanError), "MeanError"); 273 | Assert.That(maskedDiff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount"); 274 | Assert.That(maskedDiff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage"); 275 | } 276 | 277 | [TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize)] 278 | [TestCase(jpg0Rgb24, jpg1Rgb24, 0, 0, 0, 0, ResizeOption.DontResize)] 279 | [TestCase(jpg0Rgb24, jpg1Rgb24, 0, 0, 0, 0, ResizeOption.Resize)] 280 | [TestCase(pngBlack2x2px, pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize)] 281 | public void ShouldCalcDiffMaskImageSharpAndUseOutcome(string pathPic1, string pathPic2, int expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption) 282 | { 283 | var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1); 284 | var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2); 285 | var differenceMaskPicPath = Path.GetTempFileName() + "differenceMask.png"; 286 | 287 | using var absolutePic1 = Image.Load(absolutePathPic1); 288 | using var absolutePic2 = Image.Load(absolutePathPic2); 289 | 290 | using (var fileStreamDifferenceMask = File.Create(differenceMaskPicPath)) 291 | using (var maskImage = ImageSharpCompare.CalcDiffMaskImage(absolutePic1, absolutePic2, resizeOption)) 292 | { 293 | ImageExtensions.SaveAsPng(maskImage, fileStreamDifferenceMask); 294 | } 295 | 296 | using var differenceMaskPic = Image.Load(differenceMaskPicPath); 297 | var maskedDiff = ImageSharpCompare.CalcDiff(absolutePic1, absolutePic2, differenceMaskPic, resizeOption); 298 | File.Delete(differenceMaskPicPath); 299 | 300 | Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError"); 301 | Assert.That(maskedDiff.MeanError, Is.EqualTo(expectedMeanError), "MeanError"); 302 | Assert.That(maskedDiff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount"); 303 | Assert.That(maskedDiff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage"); 304 | AssertDisposeBehavior. 305 | AssertThatImageIsDisposed(absolutePic1); 306 | AssertDisposeBehavior.AssertThatImageIsDisposed(absolutePic2); 307 | AssertDisposeBehavior.AssertThatImageIsDisposed(differenceMaskPic); 308 | } 309 | 310 | [TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0, ResizeOption.DontResize)] 311 | [TestCase(jpg0Rgb24, jpg1Rgb24, 0, 0, 0, 0, ResizeOption.DontResize)] 312 | [TestCase(jpg0Rgb24, jpg1Rgb24, 0, 0, 0, 0, ResizeOption.Resize)] 313 | [TestCase(pngBlack2x2px, pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize)] 314 | public void ShouldCalcDiffMaskImageHalfSingleHalfVector2AndUseOutcome(string pathPic1, string pathPic2, int expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption) 315 | { 316 | var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1); 317 | var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2); 318 | var differenceMaskPicPath = Path.GetTempFileName() + "differenceMask.png"; 319 | 320 | using var absolutePic1 = Image.Load(absolutePathPic1); 321 | using var absolutePic2 = Image.Load(absolutePathPic2); 322 | 323 | using (var fileStreamDifferenceMask = File.Create(differenceMaskPicPath)) 324 | using (var maskImage = ImageSharpCompare.CalcDiffMaskImage(absolutePic1, absolutePic2, resizeOption)) 325 | { 326 | ImageExtensions.SaveAsPng(maskImage, fileStreamDifferenceMask); 327 | } 328 | 329 | using var differenceMaskPic = Image.Load(differenceMaskPicPath); 330 | var maskedDiff = ImageSharpCompare.CalcDiff(absolutePic1, absolutePic2, differenceMaskPic, resizeOption); 331 | File.Delete(differenceMaskPicPath); 332 | 333 | Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError"); 334 | Assert.That(maskedDiff.MeanError, Is.EqualTo(expectedMeanError), "MeanError"); 335 | Assert.That(maskedDiff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount"); 336 | Assert.That(maskedDiff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage"); 337 | AssertDisposeBehavior. 338 | AssertThatImageIsDisposed(absolutePic1); 339 | AssertDisposeBehavior.AssertThatImageIsDisposed(absolutePic2); 340 | AssertDisposeBehavior.AssertThatImageIsDisposed(differenceMaskPic); 341 | } 342 | 343 | [TestCase(pngWhite2x2px, pngBlack2x2px, pngTransparent2x2px, 765, 12240, 16, 100d, ResizeOption.Resize, 0)] 344 | [TestCase(pngWhite2x2px, pngBlack2x2px, pngBlack4x4px, 765, 12240, 16, 100d, ResizeOption.Resize, 0)] 345 | [TestCase(pngBlack2x2px, pngBlack2x2px, pngBlack4x4px, 0, 0, 0, 0, ResizeOption.Resize, 0)] 346 | [TestCase(pngBlack2x2px, pngBlack4x4px, pngBlack2x2px, 0, 0, 0, 0, ResizeOption.Resize, 0)] 347 | [TestCase(pngBlack4x4px, pngBlack2x2px, pngBlack2x2px, 0, 0, 0, 0, ResizeOption.Resize, 0)] 348 | [TestCase(colorShift1, colorShift2, pngBlack2x2px, 0, 0, 0, 0, ResizeOption.Resize, 15)] 349 | public void ShouldUseDiffMask(string pathPic1, string pathPic2, string pathPic3, double expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage, ResizeOption resizeOption, int pixelColorShiftTolerance) 350 | { 351 | var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1); 352 | var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2); 353 | var differenceMaskPic = Path.Combine(AppContext.BaseDirectory, pathPic3); 354 | using var pic1 = Image.Load(absolutePathPic1); 355 | using var pic2 = Image.Load(absolutePathPic2); 356 | using var maskPic = Image.Load(differenceMaskPic); 357 | 358 | var maskedDiff = ImageSharpCompare.CalcDiff(pic1, pic2, maskPic, resizeOption, pixelColorShiftTolerance); 359 | 360 | Assert.That(maskedDiff.MeanError, Is.EqualTo(expectedMeanError), "MeanError"); 361 | Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError"); 362 | Assert.That(maskedDiff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount"); 363 | Assert.That(maskedDiff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage"); 364 | AssertDisposeBehavior. 365 | AssertThatImageIsDisposed(pic1); 366 | AssertDisposeBehavior.AssertThatImageIsDisposed(pic2); 367 | AssertDisposeBehavior.AssertThatImageIsDisposed(maskPic); 368 | } 369 | 370 | [TestCase(pngBlack2x2px, pngBlack2x2px, pngBlack4x4px)] 371 | [TestCase(pngBlack2x2px, pngBlack4x4px, pngBlack2x2px)] 372 | [TestCase(pngBlack4x4px, pngBlack2x2px, pngBlack2x2px)] 373 | public void ShouldThrowUsingInvalidImageDimensionsDiffMask(string pathPic1, string pathPic2, string pathPic3) 374 | { 375 | var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1); 376 | var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2); 377 | var differenceMaskPic = Path.Combine(AppContext.BaseDirectory, pathPic3); 378 | using var pic1 = Image.Load(absolutePathPic1); 379 | using var pic2 = Image.Load(absolutePathPic2); 380 | using var maskPic = Image.Load(differenceMaskPic); 381 | 382 | var exception = Assert.Throws(() => ImageSharpCompare.CalcDiff(pic1, pic2, maskPic, ResizeOption.DontResize)); 383 | 384 | Assert.That(exception?.Message, Is.EqualTo("Size of images differ.")); 385 | AssertDisposeBehavior. 386 | AssertThatImageIsDisposed(pic1); 387 | AssertDisposeBehavior.AssertThatImageIsDisposed(pic2); 388 | AssertDisposeBehavior.AssertThatImageIsDisposed(maskPic); 389 | } 390 | 391 | 392 | 393 | [TestCase(png0Rgba32, png1Rgba32, 0, 0, 0, 0)] 394 | public void DiffMaskSteams(string pathPic1, string pathPic2, int expectedMeanError, int expectedAbsoluteError, int expectedPixelErrorCount, double expectedPixelErrorPercentage) 395 | { 396 | var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1); 397 | var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2); 398 | 399 | using var pic1 = new FileStream(absolutePathPic1, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 400 | using var pic2 = new FileStream(absolutePathPic2, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 401 | 402 | using var maskImage = ImageSharpCompare.CalcDiffMaskImage(pic1, pic2); 403 | 404 | pic1.Position = 0; 405 | pic2.Position = 0; 406 | 407 | var maskedDiff = ImageSharpCompare.CalcDiff(pic1, pic2, maskImage); 408 | Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(expectedAbsoluteError), "AbsoluteError"); 409 | Assert.That(maskedDiff.MeanError, Is.EqualTo(expectedMeanError), "MeanError"); 410 | Assert.That(maskedDiff.PixelErrorCount, Is.EqualTo(expectedPixelErrorCount), "PixelErrorCount"); 411 | Assert.That(maskedDiff.PixelErrorPercentage, Is.EqualTo(expectedPixelErrorPercentage), "PixelErrorPercentage"); 412 | } 413 | 414 | [TestCase(png0Rgba32, png1Rgba32)] 415 | public void CalcDiffMaskImage_WhenSupplyingDiffMaskOfTwoImagesByFilePath_NoDifferences(string image1RelativePath, string image2RelativePath) 416 | { 417 | var image1Path = Path.Combine(AppContext.BaseDirectory, image1RelativePath); 418 | var image2Path = Path.Combine(AppContext.BaseDirectory, image2RelativePath); 419 | var diffMask1Path = Path.GetTempFileName() + "differenceMask.png"; 420 | 421 | using (var diffMask1Stream = File.Create(diffMask1Path)) 422 | { 423 | using var diffMask1Image = ImageSharpCompare.CalcDiffMaskImage(image1Path, image2Path); 424 | ImageExtensions.SaveAsPng(diffMask1Image, diffMask1Stream); 425 | } 426 | 427 | using var diffMask2Image = ImageSharpCompare.CalcDiffMaskImage(image1Path, image2Path, diffMask1Path); 428 | Assert.That(IsImageEntirelyBlack(diffMask2Image), Is.True); 429 | 430 | File.Delete(diffMask1Path); 431 | } 432 | 433 | [TestCase(png0Rgba32, png1Rgba32)] 434 | public void CalcDiffMaskImage_WhenSupplyingDiffMaskOfTwoImagesByStream_NoDifferences(string image1RelativePath, string image2RelativePath) 435 | { 436 | var image1Path = Path.Combine(AppContext.BaseDirectory, image1RelativePath); 437 | var image2Path = Path.Combine(AppContext.BaseDirectory, image2RelativePath); 438 | var diffMask1Path = Path.GetTempFileName() + "differenceMask.png"; 439 | 440 | using var image1Stream = new FileStream(image1Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 441 | using var image2Stream = new FileStream(image2Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 442 | 443 | using (var diffMask1Stream = File.Create(diffMask1Path)) 444 | { 445 | using var diffMask1Image = ImageSharpCompare.CalcDiffMaskImage(image1Stream, image2Stream); 446 | ImageExtensions.SaveAsPng(diffMask1Image, diffMask1Stream); 447 | } 448 | 449 | image1Stream.Position = 0; 450 | image2Stream.Position = 0; 451 | 452 | using (var diffMask1Stream = new FileStream(diffMask1Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) 453 | { 454 | diffMask1Stream.Position = 0; 455 | using var diffMask2Image = ImageSharpCompare.CalcDiffMaskImage(image1Stream, image2Stream, diffMask1Stream); 456 | Assert.That(IsImageEntirelyBlack(diffMask2Image), Is.True); 457 | } 458 | 459 | File.Delete(diffMask1Path); 460 | } 461 | 462 | [TestCase(png0Rgba32, png1Rgba32)] 463 | public void CalcDiffMaskImage_WhenSupplyingDiffMaskOfTwoImagesByImage_NoDifferences(string image1RelativePath, string image2RelativePath) 464 | { 465 | var image1Path = Path.Combine(AppContext.BaseDirectory, image1RelativePath); 466 | var image2Path = Path.Combine(AppContext.BaseDirectory, image2RelativePath); 467 | 468 | using var image1 = Image.Load(image1Path); 469 | using var image2 = Image.Load(image2Path); 470 | 471 | using var diffMask1Image = ImageSharpCompare.CalcDiffMaskImage(image1, image2); 472 | 473 | using var diffMask2Image = ImageSharpCompare.CalcDiffMaskImage(image1, image2, diffMask1Image); 474 | 475 | Assert.That(IsImageEntirelyBlack(diffMask2Image), Is.True); 476 | } 477 | 478 | [Test] 479 | [TestCase(jpg0Rgb24, jpg1Rgb24)] 480 | [TestCase(png0Rgba32, png1Rgba32)] 481 | [TestCase(jpg0Rgb24, png1Rgba32)] 482 | [TestCase(jpg0Rgb24, png0Rgba32)] 483 | [TestCase(jpg1Rgb24, png1Rgba32)] 484 | [TestCase(colorShift1, colorShift2)] 485 | public void ShouldVerifyThatImagesAreNotEqual(string pathActual, string pathExpected) 486 | { 487 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual); 488 | var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected); 489 | 490 | Assert.That(ImageSharpCompare.ImagesAreEqual(absolutePathActual, absolutePathExpected), Is.False); 491 | } 492 | 493 | [Test] 494 | [TestCase(jpg0Rgb24, jpg1Rgb24)] 495 | [TestCase(png0Rgba32, png1Rgba32)] 496 | [TestCase(jpg0Rgb24, png1Rgba32)] 497 | [TestCase(jpg0Rgb24, png0Rgba32)] 498 | [TestCase(jpg1Rgb24, png1Rgba32)] 499 | [TestCase(colorShift1, colorShift2)] 500 | public void ShouldVerifyThatImageStreamAreNotEqual(string pathActual, string pathExpected) 501 | { 502 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pathActual); 503 | var absolutePathExpected = Path.Combine(AppContext.BaseDirectory, pathExpected); 504 | 505 | using var actual = new FileStream(absolutePathActual, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 506 | using var expected = new FileStream(absolutePathExpected, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 507 | 508 | Assert.That(ImageSharpCompare.ImagesAreEqual(actual, expected), Is.False); 509 | } 510 | 511 | [TestCase(png0Rgba32, pngBlack2x2px)] 512 | public void ShouldVerifyThatImageWithDifferentSizeThrows(string pathPic1, string pathPic2) 513 | { 514 | var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1); 515 | var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2); 516 | 517 | var exception = Assert.Throws(() => ImageSharpCompare.CalcDiff(absolutePathPic1, absolutePathPic2)); 518 | 519 | Assert.That(exception?.Message, Is.EqualTo("Size of images differ.")); 520 | } 521 | 522 | [TestCase(png0Rgba32, png0Rgba32, pngBlack2x2px)] 523 | [TestCase(png0Rgba32, pngBlack2x2px, png0Rgba32)] 524 | [TestCase(pngBlack2x2px, png0Rgba32, png0Rgba32)] 525 | public void ShouldVerifyThatImageWithDifferentSizeThrows(string pathPic1, string pathPic2, string pathPic3) 526 | { 527 | var absolutePathPic1 = Path.Combine(AppContext.BaseDirectory, pathPic1); 528 | var absolutePathPic2 = Path.Combine(AppContext.BaseDirectory, pathPic2); 529 | var absolutePathPic3 = Path.Combine(AppContext.BaseDirectory, pathPic3); 530 | 531 | var exception = Assert.Throws(() => ImageSharpCompare.CalcDiff(absolutePathPic1, absolutePathPic2, absolutePathPic3)); 532 | 533 | Assert.That(exception?.Message, Is.EqualTo("Size of images differ.")); 534 | } 535 | 536 | private static bool IsImageEntirelyBlack(Image image) 537 | { 538 | if (image is not Image imageRgb24) 539 | { 540 | throw new ArgumentException("Image must be an RGB 24 one", nameof(image)); 541 | } 542 | 543 | for (var x = 0; x < imageRgb24.Width; x++) 544 | { 545 | for (var y = 0; y < imageRgb24.Height; y++) 546 | { 547 | if (imageRgb24[x, y] != new Rgb24(byte.MinValue, byte.MinValue, byte.MinValue)) 548 | { 549 | return false; 550 | } 551 | } 552 | } 553 | 554 | return true; 555 | } 556 | } 557 | } -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/ImageSharpCompareTestNunit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0 5 | false 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/ImageSharpPixelTypeConverterTests.cs: -------------------------------------------------------------------------------- 1 | using Codeuctivity.ImageSharpCompare; 2 | using NUnit.Framework; 3 | using SixLabors.ImageSharp; 4 | using SixLabors.ImageSharp.PixelFormats; 5 | using System; 6 | using System.IO; 7 | 8 | namespace ImageSharpCompareTestNunit 9 | { 10 | public class ImageSharpPixelTypeConverterTests 11 | { 12 | private const string pngBlack2x2px = "../../../TestData/Black.png"; 13 | 14 | [Test] 15 | public void ShouldConvertToRgb24() 16 | { 17 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pngBlack2x2px); 18 | 19 | var image = Image.Load(absolutePathActual); 20 | 21 | var rgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(image, out var isClonedInstance); 22 | 23 | Assert.That(isClonedInstance, Is.False); 24 | Assert.That(rgb24.PixelType.BitsPerPixel, Is.EqualTo(24)); 25 | 26 | image.Dispose(); 27 | AssertDisposeBehavior.AssertThatImageIsDisposed(rgb24, true); 28 | } 29 | 30 | [Test] 31 | public void ShouldConvertAnyPixelTypeToRgb24() 32 | { 33 | var absolutePathActual = Path.Combine(AppContext.BaseDirectory, pngBlack2x2px); 34 | 35 | var image = Image.Load(absolutePathActual); 36 | 37 | var rgb24 = ImageSharpPixelTypeConverter.ToRgb24Image(image, out var isClonedInstance); 38 | 39 | Assert.That(isClonedInstance, Is.True); 40 | Assert.That(rgb24.PixelType.BitsPerPixel, Is.EqualTo(24)); 41 | 42 | image.Dispose(); 43 | AssertDisposeBehavior.AssertThatImageIsDisposed(rgb24); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/TestData/Black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompareTestNunit/TestData/Black.png -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/TestData/BlackDoubleSize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompareTestNunit/TestData/BlackDoubleSize.png -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/TestData/Calc0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompareTestNunit/TestData/Calc0.jpg -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/TestData/Calc0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompareTestNunit/TestData/Calc0.png -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/TestData/Calc1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompareTestNunit/TestData/Calc1.jpg -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/TestData/Calc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompareTestNunit/TestData/Calc1.png -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/TestData/ColorShift1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompareTestNunit/TestData/ColorShift1.png -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/TestData/ColorShift2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompareTestNunit/TestData/ColorShift2.png -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/TestData/HC007-Test-02-3-OxPt.html1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompareTestNunit/TestData/HC007-Test-02-3-OxPt.html1.png -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/TestData/HC007-Test-02-3-OxPt.html2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompareTestNunit/TestData/HC007-Test-02-3-OxPt.html2.png -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/TestData/White.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompareTestNunit/TestData/White.png -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/TestData/differenceMask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompareTestNunit/TestData/differenceMask.png -------------------------------------------------------------------------------- /ImageSharpCompareTestNunit/TestData/pngTransparent2x2px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Codeuctivity/ImageSharp.Compare/615a9c93d01a26ea0d5f2ff195670c7c17eddbb6/ImageSharpCompareTestNunit/TestData/pngTransparent2x2px.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Six Labors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ImageSharpCompare 2 | 3 | Compares images 4 | 5 | [![.github/workflows/dotnet.yml](https://github.com/Codeuctivity/ImageSharp.Compare/actions/workflows/dotnet.yml/badge.svg)](https://github.com/Codeuctivity/ImageSharp.Compare/actions/workflows/dotnet.yml) [![Nuget](https://img.shields.io/nuget/v/Codeuctivity.ImageSharpCompare.svg)](https://www.nuget.org/packages/Codeuctivity.ImageSharpCompare/) [![Donate](https://img.shields.io/static/v1?label=Paypal&message=Donate&color=informational)](https://www.paypal.com/donate?hosted_button_id=7M7UFMMRTS7UE) 6 | 7 | Inspired by the image compare feature "Visual verification API" of [TestApi](https://blogs.msdn.microsoft.com/ivo_manolov/2009/04/20/introduction-to-testapi-part-3-visual-verification-apis/) this code supports comparing images by using a tolerance mask image. That tolerance mask image is a valid image by itself and can be manipulated. 8 | 9 | ImageSharpCompare focus on os agnostic support and therefore depends on [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp). 10 | 11 | NOTE: The Alpha-channel is ignored. 12 | 13 | ## Example simple show cases 14 | 15 | ### Compares each RGB value of each pixel to determine the equality 16 | 17 | ```csharp 18 | bool isEqual = ImageSharpCompare.ImagesAreEqual("actual.png", "expected.png"); 19 | ``` 20 | 21 | ### Calculates diff 22 | 23 | ```csharp 24 | var calcDiff = ImageSharpCompare.CalcDiff("2x2PixelBlack.png", "2x2PixelWhite.png"); 25 | Console.WriteLine($"PixelErrorCount: {diff.PixelErrorCount}"); 26 | Console.WriteLine($"PixelErrorPercentage: {diff.PixelErrorPercentage}"); 27 | Console.WriteLine($"AbsoluteError: {diff.AbsoluteError}"); 28 | Console.WriteLine($"MeanError: {diff.MeanError}"); 29 | // PixelErrorCount: 4 30 | // PixelErrorPercentage: 100 31 | // AbsoluteError: 3060 32 | // MeanError: 765 33 | ``` 34 | 35 | ## Example show case allowing some tolerated diff 36 | 37 | Imagine two images you want to compare, and want to accept the found difference as at state of allowed difference. 38 | 39 | ### Reference Image 40 | 41 | ![actual image](./ImageSharpCompareTestNunit/TestData/Calc0.jpg "Reference Image") 42 | 43 | ### Actual Image 44 | 45 | ![actual image](./ImageSharpCompareTestNunit/TestData/Calc1.jpg "Reference Image") 46 | 47 | ### Tolerance mask image 48 | 49 | Using **CalcDiffMaskImage** you can calc a diff mask from actual and reference image 50 | 51 | Example - Create difference image 52 | 53 | ```csharp 54 | using (var fileStreamDifferenceMask = File.Create("differenceMask.png")) 55 | using (var maskImage = ImageSharpCompare.CalcDiffMaskImage(pathPic1, pathPic2)) 56 | SixLabors.ImageSharp.ImageExtensions.SaveAsPng(maskImage, fileStreamDifferenceMask); 57 | ``` 58 | 59 | ![differenceMask.png](./ImageSharpCompareTestNunit/TestData/differenceMask.png "differenceMask.png") 60 | 61 | Example - Compare two images using the created difference image. Add white pixels to differenceMask.png where you want to allow difference. 62 | 63 | ```csharp 64 | var maskedDiff = ImageSharpCompare.CalcDiff(pathPic1, pathPic2, "differenceMask.png"); 65 | Assert.That(maskedDiff.AbsoluteError, Is.EqualTo(0)); 66 | ``` 67 | -------------------------------------------------------------------------------- /cla.md: -------------------------------------------------------------------------------- 1 | # Codeuctivity Individual Contributor License Agreement 2 | 3 | Thank you for your interest in contributing to open source software projects (“Projects”) made available by Codeuctivity. This Individual Contributor License Agreement (“Agreement”) sets out the terms governing any source code, object code, bug fixes, configuration changes, tools, specifications, documentation, data, materials, feedback, information or other works of authorship that you submit or have submitted, in any form and in any manner, to Codeuctivity in respect of any of the Projects (collectively “Contributions”). 4 | 5 | You agree that the following terms apply to all of your past, present and future Contributions. Except for the licenses granted in this Agreement, you retain all of your right, title and interest in and to your Contributions. 6 | 7 | **Copyright License.** You hereby grant, and agree to grant, to Codeuctivity a non-exclusive, perpetual, irrevocable, worldwide, fully-paid, royalty-free, transferable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, and distribute your Contributions and such derivative works, with the right to sublicense the foregoing rights through multiple tiers of sublicensees. 8 | 9 | **Patent License.** You hereby grant, and agree to grant, to Codeuctivity a non-exclusive, perpetual, irrevocable, 10 | worldwide, fully-paid, royalty-free, transferable patent license to make, have made, use, offer to sell, sell, 11 | import, and otherwise transfer your Contributions, where such license applies only to those patent claims 12 | licensable by you that are necessarily infringed by your Contributions alone or by combination of your 13 | Contributions with the Project to which such Contributions were submitted, with the right to sublicense the 14 | foregoing rights through multiple tiers of sublicensees. 15 | 16 | **Moral Rights.** To the fullest extent permitted under applicable law, you hereby waive, and agree not to 17 | assert, all of your “moral rights” in or relating to your Contributions for the benefit of Codeuctivity, its assigns, and 18 | their respective direct and indirect sublicensees. 19 | 20 | **Third Party Content/Rights.** If your Contribution includes or is based on any source code, object code, bug 21 | fixes, configuration changes, tools, specifications, documentation, data, materials, feedback, information or 22 | other works of authorship that were not authored by you (“Third Party Content”) or if you are aware of any 23 | third party intellectual property or proprietary rights associated with your Contribution (“Third Party Rights”), 24 | then you agree to include with the submission of your Contribution full details respecting such Third Party 25 | Content and Third Party Rights, including, without limitation, identification of which aspects of your 26 | Contribution contain Third Party Content or are associated with Third Party Rights, the owner/author of the 27 | Third Party Content and Third Party Rights, where you obtained the Third Party Content, and any applicable 28 | third party license terms or restrictions respecting the Third Party Content and Third Party Rights. For greater 29 | certainty, the foregoing obligations respecting the identification of Third Party Content and Third Party Rights 30 | do not apply to any portion of a Project that is incorporated into your Contribution to that same Project. 31 | 32 | **Representations.** You represent that, other than the Third Party Content and Third Party Rights identified by 33 | you in accordance with this Agreement, you are the sole author of your Contributions and are legally entitled 34 | to grant the foregoing licenses and waivers in respect of your Contributions. If your Contributions were 35 | created in the course of your employment with your past or present employer(s), you represent that such 36 | employer(s) has authorized you to make your Contributions on behalf of such employer(s) or such employer 37 | (s) has waived all of their right, title or interest in or to your Contributions. 38 | 39 | **Disclaimer.** To the fullest extent permitted under applicable law, your Contributions are provided on an "asis" 40 | basis, without any warranties or conditions, express or implied, including, without limitation, any implied 41 | warranties or conditions of non-infringement, merchantability or fitness for a particular purpose. You are not 42 | required to provide support for your Contributions, except to the extent you desire to provide support. 43 | 44 | **No Obligation.** You acknowledge that Codeuctivity is under no obligation to use or incorporate your Contributions 45 | into any of the Projects. The decision to use or incorporate your Contributions into any of the Projects will be 46 | made at the sole discretion of Codeuctivity or its authorized delegates. 47 | 48 | **Disputes.** This Agreement shall be governed by and construed in accordance with the laws of Austria, without giving effect to its principles or rules regarding conflicts of laws, 49 | other than such principles directing application of Austrian law. The parties hereby submit to venue in, and 50 | jurisdiction of the courts located in Vienna, Austria for purposes relating to this Agreement. In the event 51 | that any of the provisions of this Agreement shall be held by a court or other tribunal of competent jurisdiction 52 | to be unenforceable, the remaining portions hereof shall remain in full force and effect. 53 | 54 | **Assignment.** You agree that Codeuctivity may assign this Agreement, and all of its rights, obligations and licenses 55 | hereunder. 56 | -------------------------------------------------------------------------------- /signatures/version1/cla.json: -------------------------------------------------------------------------------- 1 | { 2 | "signedContributors": [ 3 | { 4 | "name": "stesee", 5 | "id": 168659, 6 | "comment_id": 1105505346, 7 | "created_at": "2022-04-21T17:38:17Z", 8 | "repoId": 216339629, 9 | "pullRequestNo": 30 10 | }, 11 | { 12 | "name": "salarcode", 13 | "id": 1272095, 14 | "comment_id": 1163604797, 15 | "created_at": "2022-06-22T21:12:05Z", 16 | "repoId": 216339629, 17 | "pullRequestNo": 36 18 | }, 19 | { 20 | "name": "snechaev", 21 | "id": 6499856, 22 | "comment_id": 1190056766, 23 | "created_at": "2022-07-20T09:39:43Z", 24 | "repoId": 216339629, 25 | "pullRequestNo": 39 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /testenvironments.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "environments": [ 4 | { 5 | "name": "Ubuntu", 6 | "type": "wsl", 7 | "wslDistribution": "Ubuntu" 8 | } 9 | ] 10 | } 11 | --------------------------------------------------------------------------------