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
43 changes: 41 additions & 2 deletions docs/comparer.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ static Task<CompareResult> CompareImages(
return Task.FromResult(result);
}
```
<sup><a href='/src/Verify.Tests/Snippets/ComparerSnippets.cs#L51-L68' title='Snippet source file'>snippet source</a> | <a href='#snippet-ImageComparer' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/Snippets/ComparerSnippets.cs#L75-L92' title='Snippet source file'>snippet source</a> | <a href='#snippet-ImageComparer' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

The returned `CompareResult.NotEqual` takes an optional message that will be rendered in the resulting text displayed to the user on test failure.
Expand Down Expand Up @@ -103,7 +103,7 @@ VerifierSettings.RegisterStreamComparer(
compare: CompareImages);
await VerifyFile("TheImage.png");
```
<sup><a href='/src/Verify.Tests/Snippets/ComparerSnippets.cs#L41-L48' title='Snippet source file'>snippet source</a> | <a href='#snippet-StaticComparer' title='Start of snippet'>anchor</a></sup>
<sup><a href='/src/Verify.Tests/Snippets/ComparerSnippets.cs#L65-L72' title='Snippet source file'>snippet source</a> | <a href='#snippet-StaticComparer' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->


Expand Down Expand Up @@ -248,6 +248,45 @@ public Task InstanceSsimForPngFluent() =>
Dimension mismatches between the received and verified images are always reported as not equal, regardless of threshold.


### Standalone SSIM scoring

The SSIM score can also be computed directly, outside of a verification. This is useful, for example, to report a similarity metric per rendered page in a custom report:

<!-- snippet: SsimCompare -->
<a id='snippet-SsimCompare'></a>
```cs
public static double Score(Stream received, Stream verified) =>
Ssim.Compare(received, verified);
```
<sup><a href='/src/Verify.Tests/Snippets/ComparerSnippets.cs#L39-L44' title='Snippet source file'>snippet source</a> | <a href='#snippet-SsimCompare' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

`Ssim.Compare` accepts two PNG streams, two PNG byte arrays, or two `PngImage` instances decoded via `PngDecoder.Decode`. Scores range from `0` (completely different) to `1` (identical). Comparing images of different dimensions throws an `ArgumentException`.

To treat a dimension mismatch as a distinct state instead of an exception, decode with `PngDecoder.Decode` and check dimensions before comparing:

<!-- snippet: SsimCompareDimensions -->
<a id='snippet-SsimCompareDimensions'></a>
```cs
public static double? ScoreIfSameSize(Stream received, Stream verified)
{
var receivedImage = PngDecoder.Decode(received);
var verifiedImage = PngDecoder.Decode(verified);
if (receivedImage.Width != verifiedImage.Width ||
receivedImage.Height != verifiedImage.Height)
{
return null;
}

return Ssim.Compare(receivedImage, verifiedImage);
}
```
<sup><a href='/src/Verify.Tests/Snippets/ComparerSnippets.cs#L46-L61' title='Snippet source file'>snippet source</a> | <a href='#snippet-SsimCompareDimensions' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

`PngDecoder` supports the same PNG subset as the comparer (see below).


### Supported PNG variants

The bundled decoder targets the common subset of PNGs produced by test scenarios:
Expand Down
15 changes: 15 additions & 0 deletions docs/mdsource/comparer.source.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,21 @@ snippet: InstanceSsimForPng
Dimension mismatches between the received and verified images are always reported as not equal, regardless of threshold.


### Standalone SSIM scoring

The SSIM score can also be computed directly, outside of a verification. This is useful, for example, to report a similarity metric per rendered page in a custom report:

snippet: SsimCompare

`Ssim.Compare` accepts two PNG streams, two PNG byte arrays, or two `PngImage` instances decoded via `PngDecoder.Decode`. Scores range from `0` (completely different) to `1` (identical). Comparing images of different dimensions throws an `ArgumentException`.

To treat a dimension mismatch as a distinct state instead of an exception, decode with `PngDecoder.Decode` and check dimensions before comparing:

snippet: SsimCompareDimensions

`PngDecoder` supports the same PNG subset as the comparer (see below).


### Supported PNG variants

The bundled decoder targets the common subset of PNGs produced by test scenarios:
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<Project>
<PropertyGroup>
<NoWarn>CA1822;CS1591;CS0649;xUnit1026;xUnit1013;CS1573;VerifyTestsProjectDir;VerifySetParameters;PolyFillTargetsForNuget;xUnit1051;NU1608;NU1109</NoWarn>
<Version>31.25.0</Version>
<Version>31.26.0</Version>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>preview</LangVersion>
<AssemblyVersion>1.0.0</AssemblyVersion>
Expand Down
28 changes: 28 additions & 0 deletions src/Verify.Tests/Comparer/PngImageTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
public class PngImageTests
{
[Fact]
public void Valid()
{
var rgba = new byte[2 * 3 * 4];
var image = new PngImage(2, 3, rgba);
Assert.Equal(2, image.Width);
Assert.Equal(3, image.Height);
Assert.Same(rgba, image.Rgba);
}

[Fact]
public void Zero_Width_Throws() =>
Assert.Throws<ArgumentOutOfRangeException>(() => new PngImage(0, 3, []));

[Fact]
public void Zero_Height_Throws() =>
Assert.Throws<ArgumentOutOfRangeException>(() => new PngImage(3, 0, []));

[Fact]
public void Buffer_Length_Mismatch_Throws()
{
var exception = Assert.Throws<ArgumentException>(() => new PngImage(2, 2, new byte[15]));
Assert.Contains("Expected: 16", exception.Message);
Assert.Contains("Actual: 15", exception.Message);
}
}
57 changes: 57 additions & 0 deletions src/Verify.Tests/Comparer/SsimTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,63 @@ public void Large_Difference_Low_Score()
Assert.True(score < 0.5, $"Expected low score, got {score}");
}

[Fact]
public void Dimension_Mismatch_Throws()
{
var a = Solid(8, 8, 100);
var b = Solid(9, 8, 100);
var exception = Assert.Throws<ArgumentException>(() => Ssim.Compare(a, b));
Assert.Contains("8x8", exception.Message);
Assert.Contains("9x8", exception.Message);
}

[Fact]
public void Stream_Overload()
{
var png = PngTestHelper.EncodeRgba(16, 16, Random(16, 16, seed: 5).Rgba);
var score = Ssim.Compare(new MemoryStream(png), new MemoryStream(png));
Assert.Equal(1.0, score, precision: 6);
}

[Fact]
public void Stream_Overload_Dimension_Mismatch_Throws()
{
var a = PngTestHelper.EncodeRgba(10, 10, new byte[10 * 10 * 4]);
var b = PngTestHelper.EncodeRgba(11, 10, new byte[11 * 10 * 4]);
Assert.Throws<ArgumentException>(() => Ssim.Compare(new MemoryStream(a), new MemoryStream(b)));
}

[Fact]
public void Bytes_Overload()
{
var png = PngTestHelper.EncodeRgba(16, 16, Random(16, 16, seed: 6).Rgba);
var score = Ssim.Compare(png, png);
Assert.Equal(1.0, score, precision: 6);
}

[Fact]
public void Streaming_Matches_InMemory_BitForBit()
{
// The streaming Stream/byte overloads decode a band at a time; they must produce exactly the
// same score as decoding both images fully and comparing. Includes ragged sizes (not a
// multiple of the 8px window) and the sub-window tiny path.
(int w, int h)[] sizes = [(8, 8), (16, 16), (33, 17), (257, 101), (128, 128), (5, 40), (40, 5)];
foreach (var (w, h) in sizes)
{
var aRgba = Random(w, h, seed: w * 7 + h).Rgba;
var bRgba = Random(w, h, seed: w * 7 + h + 1).Rgba;

var inMemory = Ssim.Compare(new PngImage(w, h, aRgba), new PngImage(w, h, bRgba));
var streamed = Ssim.Compare(
new MemoryStream(PngTestHelper.EncodeRgba(w, h, aRgba)),
new MemoryStream(PngTestHelper.EncodeRgba(w, h, bRgba)));

Assert.Equal(
BitConverter.DoubleToInt64Bits(inMemory),
BitConverter.DoubleToInt64Bits(streamed));
}
}

[Fact]
public void Noise_Reduces_Score()
{
Expand Down
24 changes: 24 additions & 0 deletions src/Verify.Tests/Snippets/ComparerSnippets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,30 @@ public Task InstanceSsimForPngFluent() =>

#endregion

#region SsimCompare

public static double Score(Stream received, Stream verified) =>
Ssim.Compare(received, verified);

#endregion

#region SsimCompareDimensions

public static double? ScoreIfSameSize(Stream received, Stream verified)
{
var receivedImage = PngDecoder.Decode(received);
var verifiedImage = PngDecoder.Decode(verified);
if (receivedImage.Width != verifiedImage.Width ||
receivedImage.Height != verifiedImage.Height)
{
return null;
}

return Ssim.Compare(receivedImage, verifiedImage);
}

#endregion

public async Task StaticComparer()
{
#region StaticComparer
Expand Down
Loading
Loading