From b001801c2f5e4bca74f772ada2c5daf2e5fa9523 Mon Sep 17 00:00:00 2001 From: DerEffi Date: Fri, 30 Jun 2023 23:49:47 +0200 Subject: [PATCH] clean release version --- .gitattributes | 63 ++ .gitignore | 363 ++++++++++ ImageComparison.sln | 41 ++ ImageComparison/ImageComparison.csproj | 17 + ImageComparison/Models/CacheItem.cs | 30 + ImageComparison/Models/DeleteAction.cs | 9 + ImageComparison/Models/ImageAnalysis.cs | 22 + ImageComparison/Models/ImageMatch.cs | 9 + ImageComparison/Models/NoMatch.cs | 14 + ImageComparison/Models/SearchMode.cs | 11 + ImageComparison/Program.cs | 2 + ImageComparison/Services/CacheService.cs | 167 +++++ ImageComparison/Services/CompareService.cs | 235 ++++++ ImageComparison/Services/FileService.cs | 83 +++ ImageComparison/Services/HashService.cs | 105 +++ ImageComparisonGUI/.gitignore | 454 ++++++++++++ ImageComparisonGUI/App.axaml | 14 + ImageComparisonGUI/App.axaml.cs | 31 + ImageComparisonGUI/Assets/gallery.ico | Bin 0 -> 28556 bytes ImageComparisonGUI/Assets/gallery.svg | 82 +++ ImageComparisonGUI/ImageComparisonGUI.csproj | 55 ++ ImageComparisonGUI/Models/Hotkey.cs | 28 + ImageComparisonGUI/Models/HotkeyEventArgs.cs | 14 + ImageComparisonGUI/Models/HotkeyTarget.cs | 21 + ImageComparisonGUI/Models/Profile.cs | 8 + ImageComparisonGUI/Models/Settings.cs | 113 +++ ImageComparisonGUI/Pages/AboutPage.axaml | 66 ++ ImageComparisonGUI/Pages/AboutPage.axaml.cs | 18 + .../Pages/AdjustablesPage.axaml | 40 ++ .../Pages/AdjustablesPage.axaml.cs | 17 + ImageComparisonGUI/Pages/CachePage.axaml | 28 + ImageComparisonGUI/Pages/CachePage.axaml.cs | 14 + ImageComparisonGUI/Pages/HotkeysPage.axaml | 97 +++ ImageComparisonGUI/Pages/HotkeysPage.axaml.cs | 14 + ImageComparisonGUI/Pages/LocationsPage.axaml | 51 ++ .../Pages/LocationsPage.axaml.cs | 14 + ImageComparisonGUI/Pages/ProfilesPage.axaml | 43 ++ .../Pages/ProfilesPage.axaml.cs | 14 + ImageComparisonGUI/Pages/SearchPage.axaml | 137 ++++ ImageComparisonGUI/Pages/SearchPage.axaml.cs | 17 + ImageComparisonGUI/Program.cs | 22 + ImageComparisonGUI/Roots.xml | 5 + ImageComparisonGUI/Services/ConfigService.cs | 257 +++++++ ImageComparisonGUI/Services/Converter.cs | 165 +++++ ImageComparisonGUI/Services/HotkeyService.cs | 59 ++ ImageComparisonGUI/Services/UrlService.cs | 44 ++ ImageComparisonGUI/Styles/SideBar.axaml | 74 ++ ImageComparisonGUI/Styles/Styles.axaml | 34 + ImageComparisonGUI/ViewLocator.cs | 27 + .../ViewModels/AboutPageViewModel.cs | 30 + .../ViewModels/AdjustablesPageViewModel.cs | 44 ++ .../ViewModels/CachePageViewModel.cs | 48 ++ .../ViewModels/HotkeysPageViewModel.cs | 91 +++ .../ViewModels/LocationsPageViewModel.cs | 72 ++ .../ViewModels/MainWindowViewModel.cs | 41 ++ .../ViewModels/ProfilesPageViewModel.cs | 80 +++ .../ViewModels/SearchPageViewModel.cs | 291 ++++++++ .../ViewModels/ViewModelBase.cs | 7 + ImageComparisonGUI/Views/MainWindow.axaml | 76 ++ ImageComparisonGUI/Views/MainWindow.axaml.cs | 30 + ImageComparisonGUI/app.manifest | 18 + LICENSE | 674 ++++++++++++++++++ Readme.md | 53 ++ example.jpg | Bin 0 -> 344306 bytes 64 files changed, 4803 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 ImageComparison.sln create mode 100644 ImageComparison/ImageComparison.csproj create mode 100644 ImageComparison/Models/CacheItem.cs create mode 100644 ImageComparison/Models/DeleteAction.cs create mode 100644 ImageComparison/Models/ImageAnalysis.cs create mode 100644 ImageComparison/Models/ImageMatch.cs create mode 100644 ImageComparison/Models/NoMatch.cs create mode 100644 ImageComparison/Models/SearchMode.cs create mode 100644 ImageComparison/Program.cs create mode 100644 ImageComparison/Services/CacheService.cs create mode 100644 ImageComparison/Services/CompareService.cs create mode 100644 ImageComparison/Services/FileService.cs create mode 100644 ImageComparison/Services/HashService.cs create mode 100644 ImageComparisonGUI/.gitignore create mode 100644 ImageComparisonGUI/App.axaml create mode 100644 ImageComparisonGUI/App.axaml.cs create mode 100644 ImageComparisonGUI/Assets/gallery.ico create mode 100644 ImageComparisonGUI/Assets/gallery.svg create mode 100644 ImageComparisonGUI/ImageComparisonGUI.csproj create mode 100644 ImageComparisonGUI/Models/Hotkey.cs create mode 100644 ImageComparisonGUI/Models/HotkeyEventArgs.cs create mode 100644 ImageComparisonGUI/Models/HotkeyTarget.cs create mode 100644 ImageComparisonGUI/Models/Profile.cs create mode 100644 ImageComparisonGUI/Models/Settings.cs create mode 100644 ImageComparisonGUI/Pages/AboutPage.axaml create mode 100644 ImageComparisonGUI/Pages/AboutPage.axaml.cs create mode 100644 ImageComparisonGUI/Pages/AdjustablesPage.axaml create mode 100644 ImageComparisonGUI/Pages/AdjustablesPage.axaml.cs create mode 100644 ImageComparisonGUI/Pages/CachePage.axaml create mode 100644 ImageComparisonGUI/Pages/CachePage.axaml.cs create mode 100644 ImageComparisonGUI/Pages/HotkeysPage.axaml create mode 100644 ImageComparisonGUI/Pages/HotkeysPage.axaml.cs create mode 100644 ImageComparisonGUI/Pages/LocationsPage.axaml create mode 100644 ImageComparisonGUI/Pages/LocationsPage.axaml.cs create mode 100644 ImageComparisonGUI/Pages/ProfilesPage.axaml create mode 100644 ImageComparisonGUI/Pages/ProfilesPage.axaml.cs create mode 100644 ImageComparisonGUI/Pages/SearchPage.axaml create mode 100644 ImageComparisonGUI/Pages/SearchPage.axaml.cs create mode 100644 ImageComparisonGUI/Program.cs create mode 100644 ImageComparisonGUI/Roots.xml create mode 100644 ImageComparisonGUI/Services/ConfigService.cs create mode 100644 ImageComparisonGUI/Services/Converter.cs create mode 100644 ImageComparisonGUI/Services/HotkeyService.cs create mode 100644 ImageComparisonGUI/Services/UrlService.cs create mode 100644 ImageComparisonGUI/Styles/SideBar.axaml create mode 100644 ImageComparisonGUI/Styles/Styles.axaml create mode 100644 ImageComparisonGUI/ViewLocator.cs create mode 100644 ImageComparisonGUI/ViewModels/AboutPageViewModel.cs create mode 100644 ImageComparisonGUI/ViewModels/AdjustablesPageViewModel.cs create mode 100644 ImageComparisonGUI/ViewModels/CachePageViewModel.cs create mode 100644 ImageComparisonGUI/ViewModels/HotkeysPageViewModel.cs create mode 100644 ImageComparisonGUI/ViewModels/LocationsPageViewModel.cs create mode 100644 ImageComparisonGUI/ViewModels/MainWindowViewModel.cs create mode 100644 ImageComparisonGUI/ViewModels/ProfilesPageViewModel.cs create mode 100644 ImageComparisonGUI/ViewModels/SearchPageViewModel.cs create mode 100644 ImageComparisonGUI/ViewModels/ViewModelBase.cs create mode 100644 ImageComparisonGUI/Views/MainWindow.axaml create mode 100644 ImageComparisonGUI/Views/MainWindow.axaml.cs create mode 100644 ImageComparisonGUI/app.manifest create mode 100644 LICENSE create mode 100644 Readme.md create mode 100644 example.jpg diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9491a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,363 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd \ No newline at end of file diff --git a/ImageComparison.sln b/ImageComparison.sln new file mode 100644 index 0000000..33b6d5b --- /dev/null +++ b/ImageComparison.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33424.131 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageComparisonGUI", "ImageComparisonGUI\ImageComparisonGUI.csproj", "{2EB9DEEC-D4A0-47B0-B491-E2AEE3C20303}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageComparison", "ImageComparison\ImageComparison.csproj", "{A79455D8-BD6D-419A-AB94-73C4BD060B93}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2EB9DEEC-D4A0-47B0-B491-E2AEE3C20303}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2EB9DEEC-D4A0-47B0-B491-E2AEE3C20303}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2EB9DEEC-D4A0-47B0-B491-E2AEE3C20303}.Debug|x64.ActiveCfg = Debug|Any CPU + {2EB9DEEC-D4A0-47B0-B491-E2AEE3C20303}.Debug|x64.Build.0 = Debug|Any CPU + {2EB9DEEC-D4A0-47B0-B491-E2AEE3C20303}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2EB9DEEC-D4A0-47B0-B491-E2AEE3C20303}.Release|Any CPU.Build.0 = Release|Any CPU + {2EB9DEEC-D4A0-47B0-B491-E2AEE3C20303}.Release|x64.ActiveCfg = Release|Any CPU + {2EB9DEEC-D4A0-47B0-B491-E2AEE3C20303}.Release|x64.Build.0 = Release|Any CPU + {A79455D8-BD6D-419A-AB94-73C4BD060B93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A79455D8-BD6D-419A-AB94-73C4BD060B93}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A79455D8-BD6D-419A-AB94-73C4BD060B93}.Debug|x64.ActiveCfg = Debug|x64 + {A79455D8-BD6D-419A-AB94-73C4BD060B93}.Debug|x64.Build.0 = Debug|x64 + {A79455D8-BD6D-419A-AB94-73C4BD060B93}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A79455D8-BD6D-419A-AB94-73C4BD060B93}.Release|Any CPU.Build.0 = Release|Any CPU + {A79455D8-BD6D-419A-AB94-73C4BD060B93}.Release|x64.ActiveCfg = Release|x64 + {A79455D8-BD6D-419A-AB94-73C4BD060B93}.Release|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D5D12733-192D-4503-B06C-9DC05333AE02} + EndGlobalSection +EndGlobal diff --git a/ImageComparison/ImageComparison.csproj b/ImageComparison/ImageComparison.csproj new file mode 100644 index 0000000..78e2896 --- /dev/null +++ b/ImageComparison/ImageComparison.csproj @@ -0,0 +1,17 @@ + + + + Exe + net7.0 + enable + enable + AnyCPU;x64 + + + + + + + + + diff --git a/ImageComparison/Models/CacheItem.cs b/ImageComparison/Models/CacheItem.cs new file mode 100644 index 0000000..055e5e6 --- /dev/null +++ b/ImageComparison/Models/CacheItem.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ImageComparison.Models +{ + public class CacheItem + { + public string path; + public ulong scantime; + public ulong size; + public byte[]? hash; + + public ulong[] hashArray { + get { + if (hash == null) + return Array.Empty(); + + ulong[] result = new ulong[(int)Math.Ceiling((decimal)hash.Length / 8)]; + + for (int i = 0; i * 8 < hash.Length; i++) + result[i] = BitConverter.ToUInt64(hash, i * 8); + + return result; + } + } + } +} diff --git a/ImageComparison/Models/DeleteAction.cs b/ImageComparison/Models/DeleteAction.cs new file mode 100644 index 0000000..099af78 --- /dev/null +++ b/ImageComparison/Models/DeleteAction.cs @@ -0,0 +1,9 @@ +namespace ImageComparison.Models +{ + public enum DeleteAction + { + Delete, + RecycleBin, + Move + } +} diff --git a/ImageComparison/Models/ImageAnalysis.cs b/ImageComparison/Models/ImageAnalysis.cs new file mode 100644 index 0000000..3750253 --- /dev/null +++ b/ImageComparison/Models/ImageAnalysis.cs @@ -0,0 +1,22 @@ +namespace ImageComparison.Models +{ + public class ImageAnalysis + { + public FileInfo Image { get; set; } + public ulong[] Hash { get; set; } + + public byte[] HashBlob { + get { + byte[] blob = new byte[Hash.Length * 8]; + + for (int i = 0; i < Hash.Length; i++) { + byte[] bytes = BitConverter.GetBytes(Hash[i]); + for(int j = 0; j < bytes.Length; j++) + blob[i * 8 + j] = bytes[j]; + } + + return blob; + } + } + } +} diff --git a/ImageComparison/Models/ImageMatch.cs b/ImageComparison/Models/ImageMatch.cs new file mode 100644 index 0000000..4c0eba3 --- /dev/null +++ b/ImageComparison/Models/ImageMatch.cs @@ -0,0 +1,9 @@ +namespace ImageComparison.Models +{ + public class ImageMatch + { + public ImageAnalysis Image1 { get; set; } + public ImageAnalysis Image2 { get; set; } + public short Similarity { get; set; } + } +} diff --git a/ImageComparison/Models/NoMatch.cs b/ImageComparison/Models/NoMatch.cs new file mode 100644 index 0000000..643835d --- /dev/null +++ b/ImageComparison/Models/NoMatch.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ImageComparison.Models +{ + public class NoMatch + { + public string a; + public string b; + } +} diff --git a/ImageComparison/Models/SearchMode.cs b/ImageComparison/Models/SearchMode.cs new file mode 100644 index 0000000..6be42fa --- /dev/null +++ b/ImageComparison/Models/SearchMode.cs @@ -0,0 +1,11 @@ +namespace ImageComparison.Models +{ + public enum SearchMode + { + All, + Inclusive, + Exclusive, + ListInclusive, + ListExclusive + } +} diff --git a/ImageComparison/Program.cs b/ImageComparison/Program.cs new file mode 100644 index 0000000..3751555 --- /dev/null +++ b/ImageComparison/Program.cs @@ -0,0 +1,2 @@ +// See https://aka.ms/new-console-template for more information +Console.WriteLine("Hello, World!"); diff --git a/ImageComparison/Services/CacheService.cs b/ImageComparison/Services/CacheService.cs new file mode 100644 index 0000000..353fbcf --- /dev/null +++ b/ImageComparison/Services/CacheService.cs @@ -0,0 +1,167 @@ +using Dapper; +using ImageComparison.Models; +using Microsoft.Data.Sqlite; + +namespace ImageComparison.Services +{ + public static class CacheService + { + private static SqliteConnection connection; + + public static void Init() + { + try + { + Directory.CreateDirectory(FileService.DataDirectory); + connection = new SqliteConnection($"Data Source={Path.Combine(FileService.DataDirectory, "Cache.db")}"); + connection.Open(); + connection.Execute("CREATE TABLE IF NOT EXISTS file (path TEXT NOT NULL COLLATE NOCASE, scantime INTEGER NOT NULL, size INTEGER, hashtype TEXT, hash BLOB, UNIQUE(path, hashtype))"); + connection.Execute("CREATE TABLE IF NOT EXISTS nomatch (a TEXT NOT NULL COLLATE NOCASE, b TEXT NOT NULL COLLATE NOCASE, UNIQUE(a, b))"); + connection.Execute("CREATE INDEX IF NOT EXISTS idxf_ht ON file(hashtype)"); + connection.Close(); + } catch { } + } + + public static List GetImages(string hashtype) + { + List images = new(); + + try + { + connection.Open(); + + images = connection.Query("SELECT path, scantime, size, hash FROM file WHERE hashtype = @Hashtype", new { Hashtype = hashtype }).ToList(); + } catch { } + + connection.Close(); + + return images; + } + + public static void UpdateImages(List images, string hashtype, ulong scantime) + { + try + { + connection.Open(); + using (SqliteTransaction transaction = connection.BeginTransaction()) + using (SqliteCommand command = connection.CreateCommand()) + { + command.CommandText = "INSERT INTO file (path, scantime, size, hashtype, hash) VALUES (@Path, @Scantime, @Size, @Hashtype, @Hash) ON CONFLICT(path, hashtype) DO UPDATE SET scantime=@Scantime, size=@Size, hash=@Hash"; + command.Parameters.AddWithValue("@Path", "C:\\"); + command.Parameters.AddWithValue("@Scantime", 0); + command.Parameters.AddWithValue("@Size", 0); + command.Parameters.AddWithValue("@Hashtype", ""); + command.Parameters.AddWithValue("@Hash", Array.Empty()); + + images.ForEach(image => + { + command.Parameters["@Path"].Value = image.Image.FullName; + command.Parameters["@Scantime"].Value = scantime; + command.Parameters["@Size"].Value = image.Image.Length; + command.Parameters["@Hashtype"].Value = hashtype; + command.Parameters["@Hash"].Value = image.HashBlob; + command.ExecuteNonQuery(); + }); + + transaction.Commit(); + } + } + catch { } + + connection.Close(); + } + + public static void AddNoMatch(string a, string b) + { + try + { + int order = string.Compare(a, b); + if (order == 0) + return; + else if (order < 0) + (b, a) = (a, b); + + connection.Open(); + connection.Execute("INSERT INTO nomatch (a, b) VALUES (@a, @b) ON CONFLICT(a, b) DO NOTHING", new { a, b }); + } + catch { } + + connection.Close(); + } + + public static void AddNoMatches(List nomatches) + { + try + { + connection.Open(); + using (SqliteTransaction transaction = connection.BeginTransaction()) + using (SqliteCommand command = connection.CreateCommand()) + { + command.CommandText = "INSERT INTO nomatch (a, b) VALUES (@a, @b) ON CONFLICT(a, b) DO NOTHING"; + command.Parameters.AddWithValue("@a", ""); + command.Parameters.AddWithValue("@b", ""); + + nomatches.ForEach(nomatch => + { + string a, b; + int order = string.Compare(nomatch.Image1.Image.FullName, nomatch.Image2.Image.FullName); + if (order == 0) + return; + else if (order < 0) + (b, a) = (nomatch.Image1.Image.FullName, nomatch.Image2.Image.FullName); + else + (a, b) = (nomatch.Image1.Image.FullName, nomatch.Image2.Image.FullName); + + command.Parameters["@a"].Value = a; + command.Parameters["@b"].Value = b; + command.ExecuteNonQuery(); + }); + + transaction.Commit(); + } + } + catch { } + + connection.Close(); + } + + public static List GetNoMatches() + { + List nomatches = new(); + + try + { + connection.Open(); + + nomatches = connection.Query("SELECT * FROM nomatch").ToList(); + } + catch { } + + connection.Close(); + + return nomatches; + } + + public static void ClearImageCache() + { + try + { + connection.Open(); + connection.Execute("DELETE FROM file"); + } catch { } + + connection.Close(); + } + + public static void ClearNoMatchCache() + { + try + { + connection.Open(); + connection.Execute("DELETE FROM nomatch"); + } catch { } + + connection.Close(); + } + } +} diff --git a/ImageComparison/Services/CompareService.cs b/ImageComparison/Services/CompareService.cs new file mode 100644 index 0000000..9ccad41 --- /dev/null +++ b/ImageComparison/Services/CompareService.cs @@ -0,0 +1,235 @@ +using ImageComparison.Models; +using System.Timers; +using System.Collections.Concurrent; + +namespace ImageComparison.Services +{ + public class ImageComparerEventArgs + { + public int Current; + public int Target; + } + + public static class CompareService + { + public readonly static string[] SupportedFileTypes = { ".bmp", ".dib", ".jpg", ".jpeg", ".jpe", ".png", ".pbm", ".pgm", ".ppm", ".sr", ".ras", ".tiff", ".tif", ".exr", ".jp2" }; + + public static event EventHandler OnProgress = delegate {}; + + public static List> AnalyseImages(List> searchLocations, int hashDetail, bool hashBothDirections, List? cachedAnalysis, CancellationToken token = new()) + { + cachedAnalysis ??= new List(); + + List> analysed = new(); + + using (System.Timers.Timer ProgressTimer = new()) + { + int target = searchLocations.SelectMany(i => i).Count(); + + //dont overload cpu with too many threads, leave one core free + int threadCount = Environment.ProcessorCount > 1 ? Environment.ProcessorCount - 1 : 1; + + //update caller with current progress with events + ProgressTimer.Interval = 500; + ProgressTimer.AutoReset = true; + ProgressTimer.Elapsed += (object? source, ElapsedEventArgs e) => + { + OnProgress.Invoke(null, new ImageComparerEventArgs() + { + Current = analysed.SelectMany(a => a).Count(), + Target = target + }); + }; + ProgressTimer.Start(); + + //keep searchLocations separate for later comparisons depending on search mode + searchLocations.ForEach(location => + { + ConcurrentBag locationAnalysis = new(); + analysed.Add(locationAnalysis); + + Parallel.ForEach(location, new(){ MaxDegreeOfParallelism = threadCount }, file => + { + if (token.IsCancellationRequested) + return; + + try + { + CacheItem? cachedImage = cachedAnalysis.FirstOrDefault(c => c.path == file.FullName); + locationAnalysis.Add(new() + { + Image = file, + Hash = cachedImage != null && cachedImage.hash != null && cachedImage.scantime > (ulong)(file.LastWriteTime - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalSeconds ? cachedImage.hashArray : CalculateHash(file.FullName, hashDetail, hashBothDirections) + }); + } + catch (Exception e) { } + }); + }); + + ProgressTimer.Stop(); + + OnProgress.Invoke(null, new ImageComparerEventArgs() + { + Current = target, + Target = target + }); + } + + return analysed.Select(a => a.ToList()).ToList(); + } + + public static List SearchForDuplicates(List> analysedLocations, int matchThreashold, SearchMode mode, List? nomatches, CancellationToken token = new()) + { + nomatches ??= new(); + + switch(mode) + { + case SearchMode.ListExclusive: + //calculate if many locations or many files within each location given + double filesPerLocation = ((double)analysedLocations.Count * analysedLocations.SelectMany(i => i).Count()) / analysedLocations.Count; + + //dont overload cpu with too many threads, leave one core free + int threadCount = Environment.ProcessorCount > 1 ? Environment.ProcessorCount - 1 : 1; + + ConcurrentBag comparisons = new(); + + //Run in parallel if more locations than file per location, else run files within locations in parallel + Parallel.ForEach(analysedLocations, new() { MaxDegreeOfParallelism = filesPerLocation <= analysedLocations.Count ? threadCount : 1 }, (images, state, currentLocation) => + { + if (token.IsCancellationRequested) + return; + + Parallel.ForEach(images, new() { MaxDegreeOfParallelism = filesPerLocation > analysedLocations.Count ? threadCount : 1 }, (image) => + { + for(int location = (int)currentLocation + 1; location < analysedLocations.Count; location++) + { + + analysedLocations[location].ForEach(comparer => + { + if (token.IsCancellationRequested) + return; + + short similarity = CalculateSimilarity(image.Hash, comparer.Hash); + if (similarity >= matchThreashold && !IsNoMatch(nomatches, image.Image.FullName, comparer.Image.FullName)) + { + comparisons.Add(new() + { + Image1 = image, + Image2 = comparer, + Similarity = similarity + }); + } + }); + } + }); + }); + + return SortMatches(comparisons); + case SearchMode.ListInclusive: + return analysedLocations + .SelectMany(location => SearchForDuplicates(location, matchThreashold, nomatches, token)) + .ToList(); + case SearchMode.Exclusive: + return SearchForDuplicates( + analysedLocations + .SelectMany(location => + location + .GroupBy(directory => directory.Image.DirectoryName) + .Select(image => image.ToList()) + .ToList() + ) + .ToList(), + matchThreashold, + SearchMode.ListExclusive, + nomatches, + token); + case SearchMode.Inclusive: + return analysedLocations + .SelectMany(images => { + return images + .GroupBy(image => image.Image.DirectoryName) + .SelectMany(directory => SearchForDuplicates(directory.ToList(), matchThreashold, nomatches, token)); + }) + .ToList(); + default: + return SearchForDuplicates(analysedLocations.SelectMany(images => images).ToList(), matchThreashold, nomatches, token); + } + } + + public static List SearchForDuplicates(List images, int matchThreashold, List nomatches, CancellationToken token = new()) + { + nomatches ??= new(); + + ConcurrentBag comparisons = new(); + + //dont overload cpu with too many threads, leave one core free + int threadCount = Environment.ProcessorCount > 1 ? Environment.ProcessorCount - 1 : 1; + + Parallel.ForEach(images, new() { MaxDegreeOfParallelism = threadCount }, (image, state, index) => + { + for (int i = Convert.ToInt32(index) + 1; i < images.Count; i++) + { + + if (token.IsCancellationRequested) + return; + + short similarity = CalculateSimilarity(image.Hash, images[i].Hash); + if (similarity >= matchThreashold && !IsNoMatch(nomatches, image.Image.FullName, images[i].Image.FullName)) + { + comparisons.Add(new() + { + Image1 = image, + Image2 = images[i], + Similarity = similarity + }); + } + } + }); + + return SortMatches(comparisons); + } + + //Calculate Hash Values by ImageHash (Dr. Neal Krawetz algorithms) + private static ulong[] CalculateHash(string file, int detail, bool bothDirections) + { + using (Stream stream = File.OpenRead(file)) + { + return HashService.DHash(Image.Load(stream), detail, bothDirections); + } + } + + private static short CalculateSimilarity(ulong[] hash1, ulong[] hash2) + { + return HashService.Similarity(hash1, hash2); + } + + private static List SortMatches(ConcurrentBag comparisons) + { + List matches = comparisons.ToList(); + matches.Sort((a,b) => + { + int result = b.Similarity - a.Similarity; + if (result == 0) + return string.Compare(a.Image1.Image.FullName, b.Image1.Image.FullName); + + return result; + }); + + return matches; + } + + private static bool IsNoMatch(List nomatches, string a, string b) + { + if(nomatches.Count == 0) + return false; + + int order = string.Compare(a, b); + if (order == 0) + return true; + else if (order < 0) + (b, a) = (a, b); + + return nomatches.Any(n => n.a == a && n.b == b); + } + } +} diff --git a/ImageComparison/Services/FileService.cs b/ImageComparison/Services/FileService.cs new file mode 100644 index 0000000..4f52cec --- /dev/null +++ b/ImageComparison/Services/FileService.cs @@ -0,0 +1,83 @@ +using ImageComparison.Models; +using Microsoft.VisualBasic.FileIO; +using System.Collections.Immutable; + +namespace ImageComparison.Services +{ + public static class FileService + { + public static readonly string DataDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "DerEffi", "ImageComparison"); + + public static void DeleteFile(string path, DeleteAction deleteAction = DeleteAction.Delete, string target = "Duplicates\\", bool relativeTarget = true) + { + if (path == null || !File.Exists(path)) + throw new FileNotFoundException(); + + switch(deleteAction) + { + case DeleteAction.Delete: + File.Delete(path); + break; + case DeleteAction.RecycleBin: + FileSystem.DeleteFile(path, UIOption.OnlyErrorDialogs, RecycleOption.SendToRecycleBin); + break; + case DeleteAction.Move: + + if (string.IsNullOrEmpty(Path.GetDirectoryName(path))) + throw new DirectoryNotFoundException(); + + string targetPath = relativeTarget ? Path.Combine(Path.GetDirectoryName(path), target) : target; + + if (File.Exists(targetPath)) + throw new IOException(); + + if(!Directory.Exists(targetPath)) + Directory.CreateDirectory(targetPath); + + string targetFile = Path.Combine(targetPath + Path.GetFileName(path)); + int counter = 0; + while (File.Exists(targetFile)) + { + targetFile = Path.Combine(targetPath, Path.GetFileNameWithoutExtension(path) + "-" + ++counter + Path.GetExtension(path)); + } + + File.Move(path, targetFile); + + break; + } + } + + public static List> GetProcessableFiles(string[] searchLocations, bool searchSubdirectories) + { + List> directories = new(); + + foreach(string location in searchLocations) + { + List directory = new(); + + if (string.IsNullOrEmpty(location) || !Directory.Exists(location)) + continue; + + try + { + List current = Directory + .GetFiles(location, $"*.*", System.IO.SearchOption.TopDirectoryOnly) + .Where(path => CompareService.SupportedFileTypes.Any(ext => path.ToLower().EndsWith(ext))) + .Select(path => new FileInfo(path)) + .ToList(); + + if(current.Count != 0) + directory.AddRange(current); + + if(searchSubdirectories) + directory.AddRange(GetProcessableFiles(Directory.GetDirectories(location), true).SelectMany(i => i)); + } catch { } + + if (directory.Count != 0) + directories.Add(directory); + } + + return directories; + } + } +} diff --git a/ImageComparison/Services/HashService.cs b/ImageComparison/Services/HashService.cs new file mode 100644 index 0000000..9f561f3 --- /dev/null +++ b/ImageComparison/Services/HashService.cs @@ -0,0 +1,105 @@ +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using System.Diagnostics.CodeAnalysis; +using System.Collections; +using System.Numerics; +using ImageComparison.Models; + +namespace ImageComparison.Services +{ + public static class HashService + { + public static int Version = 1; + + public static ulong[] DHash(Image image, int detail = 8, bool bothDirections = false) + { + if (image == null) + { + throw new ArgumentNullException(nameof(image)); + } + + int bothDirectionsNumber = Convert.ToInt32(bothDirections); + + image.Mutate(ctx => ctx + .AutoOrient() + .Resize(detail + 1, detail + bothDirectionsNumber) + .Grayscale(GrayscaleMode.Bt601)); + + int pixelCount = detail * detail * (bothDirectionsNumber + 1); + int currentHashIndex = 0; + ulong[] hash = new ulong[(int)Math.Ceiling((double)pixelCount / 64)]; //reserve number of ulongs to hold bits of pixel comparisons + + image.ProcessPixelRows((imageAccessor) => + { + ulong mask = 1UL << 63; + Span lastRow = bothDirections ? imageAccessor.GetRowSpan(0) : null; + + for (var y = bothDirectionsNumber; y < detail + bothDirectionsNumber; y++) + { + Span row = imageAccessor.GetRowSpan(y); + Rgba32 leftPixel = row[0]; + + for (var index = 1; index < detail + 1; index++) + { + //if current ulong is full, switch to next and reset mask + if (mask == 0) + { + currentHashIndex++; + mask = 1UL << 63; + } + + Rgba32 rightPixel = row[index]; + if (leftPixel.R < rightPixel.R) + { + hash[currentHashIndex] |= mask; + } + + leftPixel = rightPixel; + mask >>= 1; + + if(bothDirections) + { + if(rightPixel.R < lastRow[index].R) + hash[currentHashIndex] |= mask; + mask >>= 1; + } + } + + if (bothDirections) + lastRow = row; + } + + }); + + return hash; + } + + public static short Similarity(ulong[] hash1, ulong[] hash2) + { + if((hash2 == null) || hash1.Length != hash2.Length) + throw new ArgumentOutOfRangeException(nameof(hash2)); + + int hashLength = hash2.Length * 64; + + return Convert.ToInt16(Math.Floor((double)(hashLength - HammingDistance(hash1, hash2)) * 10000 / hashLength)); + } + + private static int HammingDistance(ulong[] hash1, ulong[] hash2) + { + int bitcount = 0; + for(int i = 0; i < hash1.Length; i++) + { + bitcount += HammingWeight(hash1[i] ^ hash2[i]); + } + return bitcount; + } + + private static int HammingWeight(ulong i) + { + i -= ((i >> 1) & 0x5555555555555555UL); + i = (i & 0x3333333333333333UL) + ((i >> 2) & 0x3333333333333333UL); + return (int)(unchecked(((i + (i >> 4)) & 0xF0F0F0F0F0F0F0FUL) * 0x101010101010101UL) >> 56); + } + } +} diff --git a/ImageComparisonGUI/.gitignore b/ImageComparisonGUI/.gitignore new file mode 100644 index 0000000..8afdcb6 --- /dev/null +++ b/ImageComparisonGUI/.gitignore @@ -0,0 +1,454 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# Tye +.tye/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +## +## Visual studio for Mac +## + + +# globs +Makefile.in +*.userprefs +*.usertasks +config.make +config.status +aclocal.m4 +install-sh +autom4te.cache/ +*.tar.gz +tarballs/ +test-results/ + +# Mac bundle stuff +*.dmg +*.app + +# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/ImageComparisonGUI/App.axaml b/ImageComparisonGUI/App.axaml new file mode 100644 index 0000000..08908e8 --- /dev/null +++ b/ImageComparisonGUI/App.axaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/ImageComparisonGUI/App.axaml.cs b/ImageComparisonGUI/App.axaml.cs new file mode 100644 index 0000000..8b9a2d6 --- /dev/null +++ b/ImageComparisonGUI/App.axaml.cs @@ -0,0 +1,31 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using ImageComparisonGUI.Services; +using ImageComparisonGUI.Views; + +namespace ImageComparisonGUI +{ + public partial class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + ExpressionObserver.DataValidators.RemoveAll(x => x is DataAnnotationsValidationPlugin); + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } + } +} \ No newline at end of file diff --git a/ImageComparisonGUI/Assets/gallery.ico b/ImageComparisonGUI/Assets/gallery.ico new file mode 100644 index 0000000000000000000000000000000000000000..49062981297c23a323b42711edf9dd4a78a69391 GIT binary patch literal 28556 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 UYiF9^L3)6yqSlM*XV!2253kSH)c^nh literal 0 HcmV?d00001 diff --git a/ImageComparisonGUI/Assets/gallery.svg b/ImageComparisonGUI/Assets/gallery.svg new file mode 100644 index 0000000..5a41536 --- /dev/null +++ b/ImageComparisonGUI/Assets/gallery.svg @@ -0,0 +1,82 @@ + + + + diff --git a/ImageComparisonGUI/ImageComparisonGUI.csproj b/ImageComparisonGUI/ImageComparisonGUI.csproj new file mode 100644 index 0000000..e8bf4c6 --- /dev/null +++ b/ImageComparisonGUI/ImageComparisonGUI.csproj @@ -0,0 +1,55 @@ + + + WinExe + net7.0 + enable + true + app.manifest + ImageComparisonGUI.Program + DerEffi.$(AssemblyName) + Image Comparison + DerEffi + https://github.com/DerEffi/image-comparison + https://github.com/DerEffi/image-comparison + git + 2023.6.0.0 + Assets\gallery.ico + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AdjustablesPage.axaml + + + HotkeysPage.axaml + + + diff --git a/ImageComparisonGUI/Models/Hotkey.cs b/ImageComparisonGUI/Models/Hotkey.cs new file mode 100644 index 0000000..ea63996 --- /dev/null +++ b/ImageComparisonGUI/Models/Hotkey.cs @@ -0,0 +1,28 @@ +using Avalonia.Input; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ImageComparisonGUI.Models +{ + public class Hotkey + { + [JsonConverter(typeof(StringEnumConverter))] + public Key Key = Key.None; + + [JsonConverter(typeof(StringEnumConverter))] + public KeyModifiers Modifiers = KeyModifiers.None; + + [JsonConverter(typeof(StringEnumConverter))] + public HotkeyTarget Target = HotkeyTarget.None; + + public Hotkey Clone() + { + return (Hotkey)MemberwiseClone(); + } + } +} diff --git a/ImageComparisonGUI/Models/HotkeyEventArgs.cs b/ImageComparisonGUI/Models/HotkeyEventArgs.cs new file mode 100644 index 0000000..527ca86 --- /dev/null +++ b/ImageComparisonGUI/Models/HotkeyEventArgs.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ImageComparisonGUI.Models +{ + public class HotkeyEventArgs + { + public Hotkey PressedHotkey = new(); + public string SelectedPage = ""; + } +} diff --git a/ImageComparisonGUI/Models/HotkeyTarget.cs b/ImageComparisonGUI/Models/HotkeyTarget.cs new file mode 100644 index 0000000..55ecfc7 --- /dev/null +++ b/ImageComparisonGUI/Models/HotkeyTarget.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace ImageComparisonGUI.Models +{ + public enum HotkeyTarget + { + None, + SearchNoMatch, + SearchPrevious, + SearchDeleteLeft, + SearchDeleteRight, + SearchDeleteBoth, + SearchAuto, + SearchAbort, + SearchStart + } +} diff --git a/ImageComparisonGUI/Models/Profile.cs b/ImageComparisonGUI/Models/Profile.cs new file mode 100644 index 0000000..ae22b9b --- /dev/null +++ b/ImageComparisonGUI/Models/Profile.cs @@ -0,0 +1,8 @@ +namespace ImageComparisonGUI.Models +{ + public class Profile + { + public string Name; + public Settings Settings = new(); + } +} diff --git a/ImageComparisonGUI/Models/Settings.cs b/ImageComparisonGUI/Models/Settings.cs new file mode 100644 index 0000000..dfda35b --- /dev/null +++ b/ImageComparisonGUI/Models/Settings.cs @@ -0,0 +1,113 @@ +using Avalonia.Input; +using ImageComparison.Models; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using static Dapper.SqlMapper; + +namespace ImageComparisonGUI.Models +{ + public class Settings + { + //Cache settings + public bool CacheNoMatch = true; + public bool CacheImages = true; + + //Processing settings + public int MatchThreashold = 7500; + public int HashDetail = 20; + public bool HashBothDirections = true; + + //Location settings + public string[] SearchLocations = Array.Empty(); + [JsonConverter(typeof(StringEnumConverter))] + public SearchMode SearchMode = SearchMode.All; + public bool SearchSubdirectories = true; + + //Deletion settings + [JsonConverter(typeof(StringEnumConverter))] + public DeleteAction DeleteAction = DeleteAction.RecycleBin; + public string DeleteTarget = "Duplicates\\"; + public bool RelativeDeleteTarget = true; + + public List Hotkeys = new() { + new() { + Key = Key.S, + Modifiers = KeyModifiers.None, + Target = HotkeyTarget.SearchAuto + }, + new() { + Key = Key.N, + Modifiers = KeyModifiers.None, + Target = HotkeyTarget.SearchNoMatch + }, + new() { + Key = Key.A, + Modifiers = KeyModifiers.Control, + Target = HotkeyTarget.SearchAbort + }, + new() { + Key = Key.Z, + Modifiers = KeyModifiers.Control, + Target = HotkeyTarget.SearchPrevious + }, + new() { + Key = Key.Enter, + Modifiers = KeyModifiers.None, + Target = HotkeyTarget.SearchStart + }, + }; + + /// + /// Deepcopy the Settings object + /// + /// The new unrelated Settings object + public Settings Clone() + { + this.DistinguishHotkeys(); + return JsonConvert.DeserializeObject(JsonConvert.SerializeObject(this)) ?? new(); + } + + /// + /// Parse string json representation of settings to Settings object + /// + /// Read json string representation of settings from i.e. a file + /// Settings object parsed from given content or null + public static Settings? Parse(string content) + { + try + { + Settings? settings = JsonConvert.DeserializeObject(content, new JsonSerializerSettings() { ObjectCreationHandling = ObjectCreationHandling.Replace }); + if (settings != null) + { + settings.DistinguishHotkeys(); + } + return settings; + } catch { } + + return null; + } + + /// + /// Get settings as text (byte content) for saving + /// + /// UTF8 encoded text as byte array + public byte[] GetContent() + { + this.DistinguishHotkeys(); + return new UTF8Encoding(true).GetBytes(JsonConvert.SerializeObject(this, Formatting.Indented)); + } + + /// + /// Removes double Hotkey targets and double key combinations in place + /// + public void DistinguishHotkeys() + { + this.Hotkeys.RemoveAll(h => h.Key == Key.None); + this.Hotkeys = this.Hotkeys.DistinctBy(h => h.Target).DistinctBy(h => $"{h.Modifiers}-{h.Key}").ToList(); + } + } +} diff --git a/ImageComparisonGUI/Pages/AboutPage.axaml b/ImageComparisonGUI/Pages/AboutPage.axaml new file mode 100644 index 0000000..9a952a9 --- /dev/null +++ b/ImageComparisonGUI/Pages/AboutPage.axaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ImageComparisonGUI/Pages/AboutPage.axaml.cs b/ImageComparisonGUI/Pages/AboutPage.axaml.cs new file mode 100644 index 0000000..1804207 --- /dev/null +++ b/ImageComparisonGUI/Pages/AboutPage.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using CommunityToolkit.Mvvm.Input; +using ImageComparisonGUI.Services; +using ImageComparisonGUI.ViewModels; + +namespace ImageComparisonGUI.Pages +{ + public partial class AboutPage : UserControl + { + public AboutPage() + { + InitializeComponent(); + DataContext = new AboutPageViewModel(); + } + } +} diff --git a/ImageComparisonGUI/Pages/AdjustablesPage.axaml b/ImageComparisonGUI/Pages/AdjustablesPage.axaml new file mode 100644 index 0000000..c75428c --- /dev/null +++ b/ImageComparisonGUI/Pages/AdjustablesPage.axaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ImageComparisonGUI/Pages/AdjustablesPage.axaml.cs b/ImageComparisonGUI/Pages/AdjustablesPage.axaml.cs new file mode 100644 index 0000000..f8442a5 --- /dev/null +++ b/ImageComparisonGUI/Pages/AdjustablesPage.axaml.cs @@ -0,0 +1,17 @@ +using Avalonia.Controls; +using ImageComparisonGUI.ViewModels; + +namespace ImageComparisonGUI.Pages +{ + public partial class AdjustablesPage : UserControl + { + public AdjustablesPage() + { + InitializeComponent(); + + Slider matchThreasholdSlider = this.Find("MatchThreasholdSlider"); + Slider hashDetailSlider = this.Find("HashDetailSlider"); + DataContext = new AdjustablesPageViewModel(matchThreasholdSlider, hashDetailSlider); + } + } +} diff --git a/ImageComparisonGUI/Pages/CachePage.axaml b/ImageComparisonGUI/Pages/CachePage.axaml new file mode 100644 index 0000000..70a0951 --- /dev/null +++ b/ImageComparisonGUI/Pages/CachePage.axaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +