Skip to content

Commit f0619a2

Browse files
committed
Speed up image comparison
1 parent 5ad2926 commit f0619a2

7 files changed

Lines changed: 156 additions & 4 deletions

File tree

CoreTest/CoreTest.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@
2828
</ItemGroup>
2929

3030
<ItemGroup>
31-
<ProjectReference Include="..\Core\Core.csproj" />
31+
<Compile Include="..\TestShared\ImageComparer.cs" Link="ImageComparer.cs" />
32+
</ItemGroup>
33+
34+
<ItemGroup>
35+
<ProjectReference Include="..\Core\Core.csproj" />
3236
</ItemGroup>
3337

3438
</Project>

CoreTest/VerifyImages.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
//
77

88
using Codecrete.SwissQRBill.Generator;
9+
using Codecrete.SwissQRBill.Testing;
910
using Docnet.Core;
1011
using Docnet.Core.Models;
12+
using ImageMagick;
1113
using SkiaSharp;
1214
using System.Collections.Generic;
1315
using System.IO;
@@ -26,7 +28,7 @@ public class VerifyImages
2628
static VerifyImages()
2729
{
2830
VerifierSettings.RegisterStreamConverter("pdf", ConvertPdfToPng);
29-
VerifyImageMagick.RegisterComparers(threshold: 0.1, ImageMagick.ErrorMetric.PerceptualHash);
31+
ImageComparer.RegisterComparers(threshold: 0.1, ErrorMetric.PerceptualHash);
3032

3133
Settings.UseDirectory("ReferenceFiles");
3234
}

PixelCanvasTest/PixelCanvasTest.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2" PrivateAssets="all" />
2626
</ItemGroup>
2727

28+
<ItemGroup>
29+
<Compile Include="..\TestShared\ImageComparer.cs" Link="ImageComparer.cs" />
30+
</ItemGroup>
31+
2832
<ItemGroup>
2933
<ProjectReference Include="..\PixelCanvas\PixelCanvas.csproj" />
3034
</ItemGroup>

PixelCanvasTest/VerifyImages.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
// https://opensource.org/licenses/MIT
66
//
77

8+
using Codecrete.SwissQRBill.Testing;
9+
using ImageMagick;
810
using System.Runtime.CompilerServices;
911
using VerifyTests;
1012
using VerifyXunit;
@@ -17,7 +19,7 @@ protected VerifyImages() { }
1719

1820
static VerifyImages()
1921
{
20-
VerifyImageMagick.RegisterComparers(threshold: 0.35, ImageMagick.ErrorMetric.PerceptualHash);
22+
ImageComparer.RegisterComparers(threshold: 0.35, ErrorMetric.PerceptualHash);
2123

2224
Settings.UseDirectory("ReferenceFiles");
2325
}

TestShared/ImageComparer.cs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//
2+
// Swiss QR Bill Generator for .NET
3+
// Copyright (c) 2021 Manuel Bleichenbacher
4+
// Licensed under MIT License
5+
// https://opensource.org/licenses/MIT
6+
//
7+
8+
using ImageMagick;
9+
using System.Collections.Generic;
10+
using System.IO;
11+
using System.Threading.Tasks;
12+
using VerifyTests;
13+
14+
namespace Codecrete.SwissQRBill.Testing
15+
{
16+
/// <summary>
17+
/// Image comparer for Verify, comparing images with ImageMagick.
18+
/// </summary>
19+
/// <remarks>
20+
/// <para>
21+
/// This is a replacement for <c>VerifyImageMagick.RegisterComparers(...)</c>. It behaves the same
22+
/// but adds a fast path: if the received data is byte-for-byte identical to the verified data,
23+
/// the images are equal and the expensive ImageMagick comparison is skipped.
24+
/// </para>
25+
/// <para>
26+
/// The byte comparison succeeds for almost all tests as the generated output is deterministic.
27+
/// The ImageMagick comparison — in particular with the <see cref="ErrorMetric.PerceptualHash"/>
28+
/// metric, which takes seconds per A4 sized image — is then only needed for the few images
29+
/// that genuinely differ, e.g. due to a different font version or platform.
30+
/// </para>
31+
/// </remarks>
32+
public static class ImageComparer
33+
{
34+
private static readonly string[] Extensions = { "png", "jpg", "bmp", "tiff", "svg", "pdf" };
35+
36+
/// <summary>
37+
/// Registers the image comparer for the common image file extensions.
38+
/// </summary>
39+
/// <param name="threshold">Maximum difference (as reported by the error metric) for images to be considered equal.</param>
40+
/// <param name="metric">Error metric used to quantify the difference between two images.</param>
41+
public static void RegisterComparers(double threshold, ErrorMetric metric)
42+
{
43+
foreach (var extension in Extensions)
44+
{
45+
VerifierSettings.RegisterStreamComparer(
46+
extension,
47+
(received, verified, context) => Compare(received, verified, threshold, metric));
48+
}
49+
}
50+
51+
private static Task<CompareResult> Compare(Stream received, Stream verified, double threshold, ErrorMetric metric)
52+
{
53+
if (HaveEqualContent(received, verified))
54+
{
55+
return Task.FromResult(CompareResult.Equal);
56+
}
57+
58+
received.Position = 0;
59+
verified.Position = 0;
60+
61+
double? difference;
62+
using (var receivedImage = new MagickImage(received))
63+
using (var verifiedImage = new MagickImage(verified))
64+
{
65+
difference = receivedImage.Compare(verifiedImage, metric);
66+
}
67+
68+
if (difference <= threshold)
69+
{
70+
return Task.FromResult(CompareResult.Equal);
71+
}
72+
73+
return Task.FromResult(CompareResult.NotEqual($"diff({difference}) > threshold({threshold})"));
74+
}
75+
76+
private static bool HaveEqualContent(Stream stream1, Stream stream2)
77+
{
78+
stream1.Position = 0;
79+
stream2.Position = 0;
80+
81+
if (stream1.CanSeek && stream2.CanSeek && stream1.Length != stream2.Length)
82+
{
83+
return false;
84+
}
85+
86+
var buffer1 = new byte[16 * 1024];
87+
var buffer2 = new byte[16 * 1024];
88+
89+
while (true)
90+
{
91+
var length1 = ReadFully(stream1, buffer1);
92+
var length2 = ReadFully(stream2, buffer2);
93+
94+
if (length1 != length2)
95+
{
96+
return false;
97+
}
98+
99+
for (var i = 0; i < length1; i++)
100+
{
101+
if (buffer1[i] != buffer2[i])
102+
{
103+
return false;
104+
}
105+
}
106+
107+
if (length1 < buffer1.Length)
108+
{
109+
return true;
110+
}
111+
}
112+
}
113+
114+
/// <summary>
115+
/// Reads from the stream until the buffer is full or the end of the stream is reached.
116+
/// </summary>
117+
private static int ReadFully(Stream stream, byte[] buffer)
118+
{
119+
var offset = 0;
120+
while (offset < buffer.Length)
121+
{
122+
var length = stream.Read(buffer, offset, buffer.Length - offset);
123+
if (length == 0)
124+
{
125+
break;
126+
}
127+
128+
offset += length;
129+
}
130+
131+
return offset;
132+
}
133+
}
134+
}

WindowsTest/VerifyImages.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
// https://opensource.org/licenses/MIT
66
//
77

8+
using Codecrete.SwissQRBill.Testing;
9+
using ImageMagick;
810
using System;
911
using System.Collections.Generic;
1012
using System.Drawing;
@@ -23,7 +25,7 @@ protected VerifyImages() { }
2325
static VerifyImages()
2426
{
2527
VerifierSettings.RegisterStreamConverter("emf", Convert);
26-
VerifyImageMagick.RegisterComparers(threshold: 0.35, ImageMagick.ErrorMetric.PerceptualHash);
28+
ImageComparer.RegisterComparers(threshold: 0.35, ErrorMetric.PerceptualHash);
2729

2830
Settings.UseDirectory("ReferenceFiles");
2931
}

WindowsTest/WindowsTest.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2" PrivateAssets="all" />
2525
</ItemGroup>
2626

27+
<ItemGroup>
28+
<Compile Include="..\TestShared\ImageComparer.cs" Link="ImageComparer.cs" />
29+
</ItemGroup>
30+
2731
<ItemGroup>
2832
<ProjectReference Include="..\Windows\Windows.csproj" />
2933
</ItemGroup>

0 commit comments

Comments
 (0)