Skip to content

Commit c8c74b5

Browse files
committed
Fix color balance rounding
1 parent c13f5a9 commit c8c74b5

2 files changed

Lines changed: 45 additions & 5 deletions

File tree

QrCodeGenerator/Penalty.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,11 +251,12 @@ internal static int CalcColorBalance(BitMatrix modules)
251251

252252
var size = modules.Size;
253253
var totalNumber = size * size;
254-
var deviation = Math.Abs(darkModules - totalNumber / 2);
255-
var step = totalNumber / 20;
256-
// The integer division rounds down such that a proportion between 45% and 55%
257-
// does not lead to any penalty points (as per specification).
258-
var deviationSteps = deviation / step;
254+
// The deviation in percent is |darkModules / totalNumber - 1/2| * 100, and a step is
255+
// 5% of it. Scaled by 2 * totalNumber, the whole expression stays exact in integers:
256+
// the numerator is |2 * darkModules - totalNumber| * 10, the denominator is
257+
// totalNumber. The integer division rounds down such that a proportion between 45%
258+
// and 55% does not lead to any penalty points (as per specification).
259+
var deviationSteps = Math.Abs(2 * darkModules - totalNumber) * 10 / totalNumber;
259260
return 10 * deviationSteps;
260261
}
261262

QrCodeGeneratorTest/PenaltyTest.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* https://github.com/manuelbl/QrCodeGenerator
66
*/
77

8+
using System;
89
using System.Diagnostics.CodeAnalysis;
910
using Xunit;
1011

@@ -321,6 +322,33 @@ public void CalcColorBalance(int size, double percent, int expectedPenalty)
321322
Assert.Equal(expectedPenalty, Penalty.CalcColorBalance(modules));
322323
}
323324

325+
[Theory, CombinatorialData]
326+
public void CalcColorBalance_MatchesPercentageRule([CombinatorialValues(21, 29, 37, 65, 129, 177)] int size)
327+
{
328+
var totalNumber = size * size;
329+
330+
for (var darkModules = 0; darkModules <= totalNumber; darkModules += 1)
331+
{
332+
var percent = darkModules * 100.0 / totalNumber;
333+
var expectedPenalty = 10 * (int)Math.Floor(Math.Abs(percent - 50.0) / 5.0);
334+
335+
Assert.Equal(expectedPenalty, Penalty.CalcColorBalance(WithDarkCount(size, darkModules)));
336+
}
337+
}
338+
339+
[Theory, CombinatorialData]
340+
public void CalcColorBalance_IsSymmetricUnderInversion([CombinatorialValues(21, 29, 37, 65, 129, 177)] int size)
341+
{
342+
var totalNumber = size * size;
343+
344+
for (var darkModules = 0; darkModules <= totalNumber / 2; darkModules += 1)
345+
{
346+
var penalty = Penalty.CalcColorBalance(WithDarkCount(size, darkModules));
347+
348+
Assert.Equal(penalty, Penalty.CalcColorBalance(WithDarkCount(size, totalNumber - darkModules)));
349+
}
350+
}
351+
324352
[Theory, CombinatorialData]
325353
public void CalcColorBalance_BasePenalty([CombinatorialValues(21, 29, 37, 65, 129, 177)]int size)
326354
{
@@ -439,6 +467,17 @@ public void CalcFinderPattern_BasePenalty([CombinatorialValues(21, 29, 37, 65, 1
439467
Assert.Equal(0, Penalty.CalcFinderPattern(modules));
440468
}
441469

470+
// Creates a matrix with exactly the given number of dark modules.
471+
private static BitMatrix WithDarkCount(int size, int darkModules)
472+
{
473+
var result = new BitMatrix(size);
474+
for (var i = 0; i < darkModules; i += 1)
475+
{
476+
result.Set(i % size, i / size, true);
477+
}
478+
return result;
479+
}
480+
442481
private static BitMatrix Fill(int size, double percent)
443482
{
444483
var result = new BitMatrix(size);

0 commit comments

Comments
 (0)