├── .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 | [](https://github.com/Codeuctivity/ImageSharp.Compare/actions/workflows/dotnet.yml) [](https://www.nuget.org/packages/Codeuctivity.ImageSharpCompare/) [](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 | 
42 |
43 | ### Actual Image
44 |
45 | 
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 | 
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 |
--------------------------------------------------------------------------------