Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/skills/code-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# ImageComparator code review skill

Use this skill to review pull requests in this repository with project-specific context.

## Focus areas

- Validate image-comparison correctness across strategies (`legacy`, `mad`, `dhash`, `auto`).
- Check for numeric overflow risks in pixel and channel accumulation logic.
- Verify `System.Drawing` pixel-format assumptions (especially 24bpp BGR byte order and stride handling).
- Review CLI argument parsing edge cases (`--strategy`, `--benchmark`, `--benchmark-iterations`).
- Ensure Windows-only behavior is explicit for `System.Drawing` paths and tests.
- Confirm benchmark and test changes remain deterministic and minimal.

## Review output expectations

- Report only high-confidence correctness, security, or reliability issues.
- Include exact file/line references and a concrete fix suggestion for each issue.
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
dotnet-version: "10.0.x"

- name: Restore dependencies
run: dotnet restore

- name: Build
run: dotnet build --no-restore --configuration Release

- name: Run tests
run: dotnet run --no-build --configuration Release --project ImageComparator.Tests/ImageComparator.Tests.csproj

- name: Publish (self-contained)
run: dotnet publish --no-build --configuration Release --output publish
18 changes: 18 additions & 0 deletions ImageComparator.Tests/ImageComparator.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\ImageComparator\ImageComparator.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="TUnit" Version="1.64.13" />
</ItemGroup>

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
99 changes: 99 additions & 0 deletions ImageComparator.Tests/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
namespace ImageComparator.Tests;

using System.Drawing;
using System.Runtime.Versioning;
using ImageComparator;
using TUnit.Core;

public class BitmapCompareTests
{
[Test]
[SupportedOSPlatform("windows")]
public async Task StrategiesRankIdenticalAboveDifferent()
{
if (!IsWindows())
{
Skip.Test("System.Drawing comparisons are Windows-only.");
return;
}

using var imageA = CreateSplitBitmap(Color.White, Color.Black);
using var imageB = CreateSplitBitmap(Color.White, Color.Black);
using var imageC = CreateSolidBitmap(Color.Blue);

var strategies = new[]
{
ComparisonStrategy.LegacyDominantChannel,
ComparisonStrategy.MeanAbsoluteDifference,
ComparisonStrategy.DifferenceHash,
};

foreach (var strategy in strategies)
{
var comparer = new BitmapCompare(strategy);
var sameSimilarity = comparer.GetSimilarity(imageA, imageB);
var differentSimilarity = comparer.GetSimilarity(imageA, imageC);

await Assert.That(sameSimilarity > differentSimilarity).IsTrue();
}
}

[Test]
[SupportedOSPlatform("windows")]
public async Task AutoStrategyPicksDifferenceHashForLargeImages()
{
if (!IsWindows())
{
Skip.Test("System.Drawing comparisons are Windows-only.");
return;
}
using var imageA = CreateSolidBitmap(Color.Green, width: 2400, height: 1600);
using var imageB = CreateSolidBitmap(Color.Green, width: 2400, height: 1600);
var comparer = new BitmapCompare(ComparisonStrategy.Auto);

_ = comparer.GetSimilarity(imageA, imageB);

await Assert.That(comparer.LastStrategyUsed).IsEqualTo(ComparisonStrategy.DifferenceHash);
}

[Test]
[SupportedOSPlatform("windows")]
public async Task BenchmarkReturnsConcreteStrategies()
{
if (!IsWindows())
{
Skip.Test("System.Drawing comparisons are Windows-only.");
return;
}

using var imageA = CreateSolidBitmap(Color.White);
using var imageB = CreateSolidBitmap(Color.Black);
var results = ComparisonBenchmark.Run(imageA, imageB, iterations: 3);

await Assert.That(results.Count).IsEqualTo(3);
await Assert.That(results.All(result => result.Strategy != ComparisonStrategy.Auto)).IsTrue();
}

[SupportedOSPlatform("windows")]
private static Bitmap CreateSolidBitmap(Color color, int width = 64, int height = 64)
{
var bitmap = new Bitmap(width, height);
using var graphics = Graphics.FromImage(bitmap);
graphics.Clear(color);
return bitmap;
}

[SupportedOSPlatform("windows")]
private static Bitmap CreateSplitBitmap(Color leftColor, Color rightColor, int width = 64, int height = 64)
{
var bitmap = new Bitmap(width, height);
using var graphics = Graphics.FromImage(bitmap);
graphics.Clear(rightColor);
using var brush = new SolidBrush(leftColor);
graphics.FillRectangle(brush, 0, 0, width / 2, height);
return bitmap;
}

[SupportedOSPlatformGuard("windows")]
private static bool IsWindows() => OperatingSystem.IsWindows();
}
28 changes: 27 additions & 1 deletion ImageComparator.sln
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@


Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageComparator", "ImageComparator\ImageComparator.csproj", "{04C06855-4D15-4E39-B460-257E4CC0B65A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageComparator.Tests", "ImageComparator.Tests\ImageComparator.Tests.csproj", "{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Debug|x64.ActiveCfg = Debug|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Debug|x64.Build.0 = Debug|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Debug|x86.ActiveCfg = Debug|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Debug|x86.Build.0 = Debug|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Release|Any CPU.Build.0 = Release|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Release|x64.ActiveCfg = Release|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Release|x64.Build.0 = Release|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Release|x86.ActiveCfg = Release|Any CPU
{04C06855-4D15-4E39-B460-257E4CC0B65A}.Release|x86.Build.0 = Release|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Debug|x64.ActiveCfg = Debug|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Debug|x64.Build.0 = Debug|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Debug|x86.ActiveCfg = Debug|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Debug|x86.Build.0 = Debug|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Release|Any CPU.Build.0 = Release|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Release|x64.ActiveCfg = Release|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Release|x64.Build.0 = Release|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Release|x86.ActiveCfg = Release|Any CPU
{81EF9DFF-D736-4530-B67E-F942E7DBB5C9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Loading
Loading