Skip to content

Commit 324f1da

Browse files
authored
BitmapByteQRCode performance optimization (#566)
* Added benchmarks and test cases for BitmapByteQRCode * Re-wrote BitmapByteQRCode for better performance Removed loops where possible and set fixed size objects * Fixed formatting * First part of BitmapByteQRCode optimizations suggested in PR review * Second part of BitmapByteQRCode optimizations suggested in PR review * Formatted via dotnet format * Removed unused code and unnecessary ref keyword
1 parent fc48785 commit 324f1da

File tree

3 files changed

+179
-48
lines changed

3 files changed

+179
-48
lines changed

QRCoder/BitmapByteQRCode.cs

Lines changed: 79 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
42
using static QRCoder.QRCodeGenerator;
53

64
namespace QRCoder;
@@ -12,6 +10,10 @@ namespace QRCoder;
1210
// ReSharper disable once InconsistentNaming
1311
public class BitmapByteQRCode : AbstractQRCode, IDisposable
1412
{
13+
private static readonly byte[] _bitmapHeaderPart1 = new byte[] { 0x42, 0x4D };
14+
private static readonly byte[] _bitmapHeaderPart2 = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00 };
15+
private static readonly byte[] _bitmapHeaderPartEnd = new byte[] { 0x01, 0x00, 0x18, 0x00 };
16+
1517
/// <summary>
1618
/// Initializes a new instance of the <see cref="BitmapByteQRCode"/> class.
1719
/// Constructor without parameters to be used in COM objects connections.
@@ -53,54 +55,83 @@ public byte[] GetGraphic(int pixelsPerModule, byte[] darkColorRgb, byte[] lightC
5355
{
5456
var sideLength = QrCodeData.ModuleMatrix.Count * pixelsPerModule;
5557

56-
var moduleDark = darkColorRgb.Reverse();
57-
var moduleLight = lightColorRgb.Reverse();
58+
// Pre-calculate color/module bytes
59+
byte[] moduleDark = new byte[pixelsPerModule * 3];
60+
byte[] moduleLight = new byte[pixelsPerModule * 3];
61+
for (int i = 0; i < pixelsPerModule * 3; i += 3)
62+
{
63+
moduleDark[i] = darkColorRgb[2];
64+
moduleDark[i + 1] = darkColorRgb[1];
65+
moduleDark[i + 2] = darkColorRgb[0];
66+
moduleLight[i] = lightColorRgb[2];
67+
moduleLight[i + 1] = lightColorRgb[1];
68+
moduleLight[i + 2] = lightColorRgb[0];
69+
}
70+
71+
// Pre-calculate padding bytes
72+
var paddingLen = sideLength % 4;
73+
74+
// Calculate filesize (header + pixel data + padding)
75+
var fileSize = 54 + (3 * (sideLength * sideLength)) + (sideLength * paddingLen);
76+
77+
// Bitmap container
78+
byte[] bmp = new byte[fileSize];
79+
int ix = 0;
80+
81+
// Header part 1
82+
Array.Copy(_bitmapHeaderPart1, 0, bmp, ix, _bitmapHeaderPart1.Length);
83+
ix += _bitmapHeaderPart1.Length;
84+
85+
// Filesize
86+
CopyIntAs4ByteToArray(fileSize, ix, bmp);
87+
ix += 4;
5888

59-
var bmp = new List<byte>();
89+
// Header part 2
90+
Array.Copy(_bitmapHeaderPart2, 0, bmp, ix, _bitmapHeaderPart2.Length);
91+
ix += _bitmapHeaderPart2.Length;
6092

61-
//header
62-
bmp.AddRange(new byte[] { 0x42, 0x4D, 0x4C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00 });
93+
// Width
94+
CopyIntAs4ByteToArray(sideLength, ix, bmp);
95+
ix += 4;
96+
// Height
97+
CopyIntAs4ByteToArray(sideLength, ix, bmp);
98+
ix += 4;
6399

64-
//width
65-
bmp.AddRange(IntTo4Byte(sideLength));
66-
//height
67-
bmp.AddRange(IntTo4Byte(sideLength));
100+
// Header end
101+
Array.Copy(_bitmapHeaderPartEnd, 0, bmp, ix, _bitmapHeaderPartEnd.Length);
102+
ix += _bitmapHeaderPartEnd.Length;
68103

69-
//header end
70-
bmp.AddRange(new byte[] { 0x01, 0x00, 0x18, 0x00 });
71-
bmp.AddRange(new byte[24]);
104+
// Add header null-bytes
105+
ix += 24;
72106

73-
//draw qr code
107+
108+
// Draw qr code
74109
for (var x = sideLength - 1; x >= 0; x -= pixelsPerModule)
75110
{
76-
for (int pm = 0; pm < pixelsPerModule; pm++)
111+
var modMatrixX = (x + pixelsPerModule) / pixelsPerModule - 1;
112+
113+
// Write data for first pixel of pixelsPerModule
114+
int posStartFirstPx = ix;
115+
for (var y = 0; y < sideLength; y += pixelsPerModule)
77116
{
78-
for (var y = 0; y < sideLength; y += pixelsPerModule)
79-
{
80-
var module =
81-
QrCodeData.ModuleMatrix[(x + pixelsPerModule) / pixelsPerModule - 1][(y + pixelsPerModule) / pixelsPerModule - 1];
82-
for (int i = 0; i < pixelsPerModule; i++)
83-
{
84-
bmp.AddRange(module ? moduleDark : moduleLight);
85-
}
86-
}
87-
if (sideLength % 4 != 0)
88-
{
89-
for (int i = 0; i < sideLength % 4; i++)
90-
{
91-
bmp.Add(0x00);
92-
}
93-
}
117+
var module = QrCodeData.ModuleMatrix[modMatrixX][(y + pixelsPerModule) / pixelsPerModule - 1];
118+
Array.Copy(module ? moduleDark : moduleLight, 0, bmp, ix, moduleDark.Length);
119+
ix += moduleDark.Length;
94120
}
95-
}
121+
// Add padding (to make line length a multiple of 4)
122+
ix += paddingLen;
123+
int lenFirstPx = ix - posStartFirstPx;
96124

97-
// write filesize in header
98-
var bmpFileSize = IntTo4Byte(bmp.Count);
99-
for (int i = 0; i < bmpFileSize.Length; i++)
100-
{
101-
bmp[2 + i] = bmpFileSize[i];
125+
// Re-write (copy) first pixel (pixelsPerModule - 1) times
126+
for (int pm = 0; pm < (pixelsPerModule - 1); pm++)
127+
{
128+
// Draw pixels
129+
Array.Copy(bmp, posStartFirstPx, bmp, ix, lenFirstPx);
130+
ix += lenFirstPx;
131+
}
102132
}
103-
return bmp.ToArray();
133+
134+
return bmp;
104135
}
105136

106137

@@ -119,22 +150,22 @@ private byte[] HexColorToByteArray(string colorString)
119150
return byteColor;
120151
}
121152

153+
122154
/// <summary>
123-
/// Converts an integer to a 4-byte array.
155+
/// Converts an integer to a 4 bytes and writes them to a byte array at given position
124156
/// </summary>
125157
/// <param name="inp">The integer to convert.</param>
126-
/// <returns>Returns the integer as a 4-byte array.</returns>
127-
private byte[] IntTo4Byte(int inp)
158+
/// <param name="destinationIndex">Index of destinationArray where the converted bytes are written to</param>
159+
/// <param name="destinationArray">Destination byte array that receives the bytes</param>
160+
private void CopyIntAs4ByteToArray(int inp, int destinationIndex, byte[] destinationArray)
128161
{
129-
byte[] bytes = new byte[4];
130162
unchecked
131163
{
132-
bytes[3] = (byte)(inp >> 24);
133-
bytes[2] = (byte)(inp >> 16);
134-
bytes[1] = (byte)(inp >> 8);
135-
bytes[0] = (byte)(inp);
164+
destinationArray[destinationIndex + 3] = (byte)(inp >> 24);
165+
destinationArray[destinationIndex + 2] = (byte)(inp >> 16);
166+
destinationArray[destinationIndex + 1] = (byte)(inp >> 8);
167+
destinationArray[destinationIndex + 0] = (byte)(inp);
136168
}
137-
return bytes;
138169
}
139170
}
140171

QRCoderBenchmarks/BitmapByteQRCode.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using BenchmarkDotNet.Attributes;
2+
using QRCoder;
3+
4+
namespace QRCoderBenchmarks;
5+
6+
[MemoryDiagnoser]
7+
public class BitmapByteQRCodeBenchmark
8+
{
9+
private readonly Dictionary<string, QRCodeData> _samples;
10+
11+
public BitmapByteQRCodeBenchmark()
12+
{
13+
var eccLvl = QRCoder.QRCodeGenerator.ECCLevel.L;
14+
_samples = new Dictionary<string, QRCodeData>()
15+
{
16+
{ "small", QRCoder.QRCodeGenerator.GenerateQrCode("ABCD", eccLvl) },
17+
{ "medium", QRCoder.QRCodeGenerator.GenerateQrCode("https://github.com/codebude/QRCoder/blob/f89aa90081f369983a9ba114e49cc6ebf0b2a7b1/QRCoder/Framework4.0Methods/Stream4Methods.cs", eccLvl) },
18+
{ "big", QRCoder.QRCodeGenerator.GenerateQrCode( new string('a', 2600), eccLvl) }
19+
};
20+
}
21+
22+
23+
[Benchmark]
24+
public void RenderBitmapByteQRCodeSmall()
25+
{
26+
var qrCode = new BitmapByteQRCode(_samples["small"]);
27+
_ = qrCode.GetGraphic(10);
28+
}
29+
30+
[Benchmark]
31+
public void RenderBitmapByteQRCodeMedium()
32+
{
33+
var qrCode = new BitmapByteQRCode(_samples["medium"]);
34+
_ = qrCode.GetGraphic(10);
35+
}
36+
37+
[Benchmark]
38+
public void RenderBitmapByteQRCodeBig()
39+
{
40+
var qrCode = new BitmapByteQRCode(_samples["big"]);
41+
_ = qrCode.GetGraphic(10);
42+
}
43+
44+
[Benchmark]
45+
public void RenderBitmapByteQRCodeHuge()
46+
{
47+
var qrCode = new BitmapByteQRCode(_samples["big"]);
48+
_ = qrCode.GetGraphic(50);
49+
}
50+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using QRCoder;
2+
using QRCoderTests.Helpers;
3+
using QRCoderTests.Helpers.XUnitExtenstions;
4+
using Shouldly;
5+
using Xunit;
6+
7+
8+
namespace QRCoderTests;
9+
10+
11+
public class BitmapByteQRCodeRendererTests
12+
{
13+
[Fact]
14+
[Category("QRRenderer/BitmapByteQRCode")]
15+
public void can_render_bitmapbyte_qrcode()
16+
{
17+
var gen = new QRCodeGenerator();
18+
var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H);
19+
var bmp = new BitmapByteQRCode(data).GetGraphic(10);
20+
21+
var result = HelperFunctions.ByteArrayToHash(bmp);
22+
result.ShouldBe("2d262d074f5c436ad93025150392dd38");
23+
}
24+
25+
26+
[Fact]
27+
[Category("QRRenderer/BitmapByteQRCode")]
28+
public void can_render_bitmapbyte_qrcode_color_bytearray()
29+
{
30+
var gen = new QRCodeGenerator();
31+
var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H);
32+
var bmp = new BitmapByteQRCode(data).GetGraphic(10, new byte[] { 30, 30, 30 }, new byte[] { 255, 0, 0 });
33+
34+
var result = HelperFunctions.ByteArrayToHash(bmp);
35+
result.ShouldBe("1184507c7eb98f9ca76afd04313c41cb");
36+
}
37+
38+
[Fact]
39+
[Category("QRRenderer/BitmapByteQRCode")]
40+
public void can_render_bitmapbyte_qrcode_drawing_color()
41+
{
42+
var gen = new QRCodeGenerator();
43+
var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H);
44+
var bmp = new BitmapByteQRCode(data).GetGraphic(10, "#e3e3e3", "#ffffff");
45+
46+
var result = HelperFunctions.ByteArrayToHash(bmp);
47+
result.ShouldBe("40cd208fc46aa726d6e98a2028ffd2b7");
48+
}
49+
50+
}

0 commit comments

Comments
 (0)