Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BitmapByteQRCode performance optimization #566

Merged
merged 7 commits into from
Jun 23, 2024
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
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");
}

}