Skip to content

Commit

Permalink
BitmapByteQRCode performance optimization (#566)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
codebude authored Jun 23, 2024
1 parent fc48785 commit 324f1da
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 48 deletions.
127 changes: 79 additions & 48 deletions QRCoder/BitmapByteQRCode.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using static QRCoder.QRCodeGenerator;

namespace QRCoder;
Expand All @@ -12,6 +10,10 @@ namespace QRCoder;
// ReSharper disable once InconsistentNaming
public class BitmapByteQRCode : AbstractQRCode, IDisposable
{
private static readonly byte[] _bitmapHeaderPart1 = new byte[] { 0x42, 0x4D };
private static readonly byte[] _bitmapHeaderPart2 = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00 };
private static readonly byte[] _bitmapHeaderPartEnd = new byte[] { 0x01, 0x00, 0x18, 0x00 };

/// <summary>
/// Initializes a new instance of the <see cref="BitmapByteQRCode"/> class.
/// Constructor without parameters to be used in COM objects connections.
Expand Down Expand Up @@ -53,54 +55,83 @@ public byte[] GetGraphic(int pixelsPerModule, byte[] darkColorRgb, byte[] lightC
{
var sideLength = QrCodeData.ModuleMatrix.Count * pixelsPerModule;

var moduleDark = darkColorRgb.Reverse();
var moduleLight = lightColorRgb.Reverse();
// Pre-calculate color/module bytes
byte[] moduleDark = new byte[pixelsPerModule * 3];
byte[] moduleLight = new byte[pixelsPerModule * 3];
for (int i = 0; i < pixelsPerModule * 3; i += 3)
{
moduleDark[i] = darkColorRgb[2];
moduleDark[i + 1] = darkColorRgb[1];
moduleDark[i + 2] = darkColorRgb[0];
moduleLight[i] = lightColorRgb[2];
moduleLight[i + 1] = lightColorRgb[1];
moduleLight[i + 2] = lightColorRgb[0];
}

// Pre-calculate padding bytes
var paddingLen = sideLength % 4;

// Calculate filesize (header + pixel data + padding)
var fileSize = 54 + (3 * (sideLength * sideLength)) + (sideLength * paddingLen);

// Bitmap container
byte[] bmp = new byte[fileSize];
int ix = 0;

// Header part 1
Array.Copy(_bitmapHeaderPart1, 0, bmp, ix, _bitmapHeaderPart1.Length);
ix += _bitmapHeaderPart1.Length;

// Filesize
CopyIntAs4ByteToArray(fileSize, ix, bmp);
ix += 4;

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

//header
bmp.AddRange(new byte[] { 0x42, 0x4D, 0x4C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00 });
// Width
CopyIntAs4ByteToArray(sideLength, ix, bmp);
ix += 4;
// Height
CopyIntAs4ByteToArray(sideLength, ix, bmp);
ix += 4;

//width
bmp.AddRange(IntTo4Byte(sideLength));
//height
bmp.AddRange(IntTo4Byte(sideLength));
// Header end
Array.Copy(_bitmapHeaderPartEnd, 0, bmp, ix, _bitmapHeaderPartEnd.Length);
ix += _bitmapHeaderPartEnd.Length;

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

//draw qr code

// Draw qr code
for (var x = sideLength - 1; x >= 0; x -= pixelsPerModule)
{
for (int pm = 0; pm < pixelsPerModule; pm++)
var modMatrixX = (x + pixelsPerModule) / pixelsPerModule - 1;

// Write data for first pixel of pixelsPerModule
int posStartFirstPx = ix;
for (var y = 0; y < sideLength; y += pixelsPerModule)
{
for (var y = 0; y < sideLength; y += pixelsPerModule)
{
var module =
QrCodeData.ModuleMatrix[(x + pixelsPerModule) / pixelsPerModule - 1][(y + pixelsPerModule) / pixelsPerModule - 1];
for (int i = 0; i < pixelsPerModule; i++)
{
bmp.AddRange(module ? moduleDark : moduleLight);
}
}
if (sideLength % 4 != 0)
{
for (int i = 0; i < sideLength % 4; i++)
{
bmp.Add(0x00);
}
}
var module = QrCodeData.ModuleMatrix[modMatrixX][(y + pixelsPerModule) / pixelsPerModule - 1];
Array.Copy(module ? moduleDark : moduleLight, 0, bmp, ix, moduleDark.Length);
ix += moduleDark.Length;
}
}
// Add padding (to make line length a multiple of 4)
ix += paddingLen;
int lenFirstPx = ix - posStartFirstPx;

// write filesize in header
var bmpFileSize = IntTo4Byte(bmp.Count);
for (int i = 0; i < bmpFileSize.Length; i++)
{
bmp[2 + i] = bmpFileSize[i];
// Re-write (copy) first pixel (pixelsPerModule - 1) times
for (int pm = 0; pm < (pixelsPerModule - 1); pm++)
{
// Draw pixels
Array.Copy(bmp, posStartFirstPx, bmp, ix, lenFirstPx);
ix += lenFirstPx;
}
}
return bmp.ToArray();

return bmp;
}


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


/// <summary>
/// Converts an integer to a 4-byte array.
/// Converts an integer to a 4 bytes and writes them to a byte array at given position
/// </summary>
/// <param name="inp">The integer to convert.</param>
/// <returns>Returns the integer as a 4-byte array.</returns>
private byte[] IntTo4Byte(int inp)
/// <param name="destinationIndex">Index of destinationArray where the converted bytes are written to</param>
/// <param name="destinationArray">Destination byte array that receives the bytes</param>
private void CopyIntAs4ByteToArray(int inp, int destinationIndex, byte[] destinationArray)
{
byte[] bytes = new byte[4];
unchecked
{
bytes[3] = (byte)(inp >> 24);
bytes[2] = (byte)(inp >> 16);
bytes[1] = (byte)(inp >> 8);
bytes[0] = (byte)(inp);
destinationArray[destinationIndex + 3] = (byte)(inp >> 24);
destinationArray[destinationIndex + 2] = (byte)(inp >> 16);
destinationArray[destinationIndex + 1] = (byte)(inp >> 8);
destinationArray[destinationIndex + 0] = (byte)(inp);
}
return bytes;
}
}

Expand Down
50 changes: 50 additions & 0 deletions QRCoderBenchmarks/BitmapByteQRCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using BenchmarkDotNet.Attributes;
using QRCoder;

namespace QRCoderBenchmarks;

[MemoryDiagnoser]
public class BitmapByteQRCodeBenchmark
{
private readonly Dictionary<string, QRCodeData> _samples;

public BitmapByteQRCodeBenchmark()
{
var eccLvl = QRCoder.QRCodeGenerator.ECCLevel.L;
_samples = new Dictionary<string, QRCodeData>()
{
{ "small", QRCoder.QRCodeGenerator.GenerateQrCode("ABCD", eccLvl) },
{ "medium", QRCoder.QRCodeGenerator.GenerateQrCode("https://github.com/codebude/QRCoder/blob/f89aa90081f369983a9ba114e49cc6ebf0b2a7b1/QRCoder/Framework4.0Methods/Stream4Methods.cs", eccLvl) },
{ "big", QRCoder.QRCodeGenerator.GenerateQrCode( new string('a', 2600), eccLvl) }
};
}


[Benchmark]
public void RenderBitmapByteQRCodeSmall()
{
var qrCode = new BitmapByteQRCode(_samples["small"]);
_ = qrCode.GetGraphic(10);
}

[Benchmark]
public void RenderBitmapByteQRCodeMedium()
{
var qrCode = new BitmapByteQRCode(_samples["medium"]);
_ = qrCode.GetGraphic(10);
}

[Benchmark]
public void RenderBitmapByteQRCodeBig()
{
var qrCode = new BitmapByteQRCode(_samples["big"]);
_ = qrCode.GetGraphic(10);
}

[Benchmark]
public void RenderBitmapByteQRCodeHuge()
{
var qrCode = new BitmapByteQRCode(_samples["big"]);
_ = qrCode.GetGraphic(50);
}
}
50 changes: 50 additions & 0 deletions QRCoderTests/BitmapByteQRCodeRendererTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using QRCoder;
using QRCoderTests.Helpers;
using QRCoderTests.Helpers.XUnitExtenstions;
using Shouldly;
using Xunit;


namespace QRCoderTests;


public class BitmapByteQRCodeRendererTests
{
[Fact]
[Category("QRRenderer/BitmapByteQRCode")]
public void can_render_bitmapbyte_qrcode()
{
var gen = new QRCodeGenerator();
var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H);
var bmp = new BitmapByteQRCode(data).GetGraphic(10);

var result = HelperFunctions.ByteArrayToHash(bmp);
result.ShouldBe("2d262d074f5c436ad93025150392dd38");
}


[Fact]
[Category("QRRenderer/BitmapByteQRCode")]
public void can_render_bitmapbyte_qrcode_color_bytearray()
{
var gen = new QRCodeGenerator();
var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H);
var bmp = new BitmapByteQRCode(data).GetGraphic(10, new byte[] { 30, 30, 30 }, new byte[] { 255, 0, 0 });

var result = HelperFunctions.ByteArrayToHash(bmp);
result.ShouldBe("1184507c7eb98f9ca76afd04313c41cb");
}

[Fact]
[Category("QRRenderer/BitmapByteQRCode")]
public void can_render_bitmapbyte_qrcode_drawing_color()
{
var gen = new QRCodeGenerator();
var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.H);
var bmp = new BitmapByteQRCode(data).GetGraphic(10, "#e3e3e3", "#ffffff");

var result = HelperFunctions.ByteArrayToHash(bmp);
result.ShouldBe("40cd208fc46aa726d6e98a2028ffd2b7");
}

}

0 comments on commit 324f1da

Please sign in to comment.