Skip to content
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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<LangVersion>10</LangVersion>
<LangVersion>12</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsNotAsErrors>$(WarningsNotAsErrors);IDE0005</WarningsNotAsErrors>
<CheckEolTargetFramework>false</CheckEolTargetFramework>
Expand Down
126 changes: 73 additions & 53 deletions QRCoder/QRCodeData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,42 +59,54 @@ public QRCodeData(string pathToRawData, Compression compressMode) : this(File.Re
/// <param name="compressMode">The compression mode used for the raw data.</param>
public QRCodeData(byte[] rawData, Compression compressMode)
{
var bytes = new List<byte>(rawData);

//Decompress
if (compressMode == Compression.Deflate)
{
using var input = new MemoryStream(bytes.ToArray());
using var input = new MemoryStream(rawData);
using var output = new MemoryStream();
using (var dstream = new DeflateStream(input, CompressionMode.Decompress))
{
dstream.CopyTo(output);
}
bytes = new List<byte>(output.ToArray());
rawData = output.ToArray();
}
else if (compressMode == Compression.GZip)
{
using var input = new MemoryStream(bytes.ToArray());
using var input = new MemoryStream(rawData);
using var output = new MemoryStream();
using (var dstream = new GZipStream(input, CompressionMode.Decompress))
{
dstream.CopyTo(output);
}
bytes = new List<byte>(output.ToArray());
rawData = output.ToArray();
}

if (bytes[0] != 0x51 || bytes[1] != 0x52 || bytes[2] != 0x52)
if (rawData.Length < 5)
throw new Exception("Invalid raw data file. File too short.");
if (rawData[0] != 0x51 || rawData[1] != 0x52 || rawData[2] != 0x52)
throw new Exception("Invalid raw data file. Filetype doesn't match \"QRR\".");

//Set QR code version
var sideLen = (int)bytes[4];
bytes.RemoveRange(0, 5);
Version = (sideLen - 21 - 8) / 4 + 1;
// Set QR code version from side length (includes 8-module quiet zone)
var sideLen = (int)rawData[4];
if (sideLen < 29) // Micro QR: sideLen = 19 + 2*(m-1), m in [1..4] -> versions -1..-4
{
if (((sideLen - 19) & 1) != 0)
throw new Exception("Invalid raw data file. Side length not valid for Micro QR.");
var m = ((sideLen - 19) / 2) + 1;
Version = -m;
}
else // Standard QR: sideLen = 29 + 4*(v-1), v in [1..40]
{
if (((sideLen - 29) % 4) != 0)
throw new Exception("Invalid raw data file. Side length not valid for QR.");
Version = ((sideLen - 29) / 4) + 1;
}

//Unpack
var modules = new Queue<bool>(8 * bytes.Count);
foreach (var b in bytes)
var modules = new Queue<bool>(8 * (rawData.Length - 5));
for (int j = 5; j < rawData.Length; j++)
{
var b = rawData[j];
for (int i = 7; i >= 0; i--)
{
modules.Enqueue((b & (1 << i)) != 0);
Expand All @@ -111,7 +123,6 @@ public QRCodeData(byte[] rawData, Compression compressMode)
ModuleMatrix[y][x] = modules.Dequeue();
}
}

}

/// <summary>
Expand All @@ -121,60 +132,69 @@ public QRCodeData(byte[] rawData, Compression compressMode)
/// <returns>Returns the raw data of the QR code as a byte array.</returns>
public byte[] GetRawData(Compression compressMode)
{
var bytes = new List<byte>();

//Add header - signature ("QRR")
bytes.AddRange(new byte[] { 0x51, 0x52, 0x52, 0x00 });
using var output = new MemoryStream();
Stream targetStream = output;
DeflateStream? deflateStream = null;
GZipStream? gzipStream = null;

//Add header - rowsize
bytes.Add((byte)ModuleMatrix.Count);

//Build data queue
var dataQueue = new Queue<int>();
foreach (var row in ModuleMatrix)
//Set up compression stream if needed
if (compressMode == Compression.Deflate)
{
foreach (var module in row)
{
dataQueue.Enqueue((bool)module ? 1 : 0);
}
deflateStream = new DeflateStream(output, CompressionMode.Compress, true);
targetStream = deflateStream;
}
for (int i = 0; i < 8 - (ModuleMatrix.Count * ModuleMatrix.Count) % 8; i++)
else if (compressMode == Compression.GZip)
{
dataQueue.Enqueue(0);
gzipStream = new GZipStream(output, CompressionMode.Compress, true);
targetStream = gzipStream;
}

//Process queue
while (dataQueue.Count > 0)
try
{
byte b = 0;
for (int i = 7; i >= 0; i--)
//Add header - signature ("QRR")
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
targetStream.Write([0x51, 0x52, 0x52, 0x00]);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just as note: for .NET less than 8 this will still allocate the array, as the collection builder, etc. isn't available in .NET, so Roslyn can't use these types and produces "fallback-code".
The change is still good, as it clearer to read. Thanks.

#else
targetStream.Write(new byte[] { 0x51, 0x52, 0x52, 0x00 }, 0, 4);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use collection expression to avoid the byte[] allocation for .NET 8+ (where they were introduced).

Suggested change
targetStream.Write(new byte[] { 0x51, 0x52, 0x52, 0x00 }, 0, 4);
#if NET8_OR_GREATER
targetStream.Write([0x51, 0x52, 0x52, 0x00]);
#else
targetStream.Write(new byte[] { 0x51, 0x52, 0x52, 0x00 }, 0, 4);
#endif

I thinks .NET 8+ is finde, as older targets aren't supported anymore by .NET.
Else, we could keep the signature in a static readonly and load it from there. But not so nice code...

Also .NET 10's JIT would stack allocate that array, so no heap allocation.
But the collection expression reads much nicer.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I bumped the C# version to 12, so the code you supplied will work as far back as .NET Core 2.1 -- anything supported by Span. I'll update the conditional again to HAS_SPAN in a future PR, and probably add netstandard2_1 as a target at the same time.

#endif

//Add header - rowsize
targetStream.WriteByte((byte)ModuleMatrix.Count);

//Build data queue
var dataQueue = new Queue<int>();
foreach (var row in ModuleMatrix)
{
b += (byte)(dataQueue.Dequeue() << i);
foreach (var module in row)
{
dataQueue.Enqueue((bool)module ? 1 : 0);
}
}
int mod = (int)(((uint)ModuleMatrix.Count * (uint)ModuleMatrix.Count) % 8);
for (int i = 0; i < 8 - mod; i++)
{
dataQueue.Enqueue(0);
}
bytes.Add(b);
}
var rawData = bytes.ToArray();

//Compress stream (optional)
if (compressMode == Compression.Deflate)
{
using var output = new MemoryStream();
using (var dstream = new DeflateStream(output, CompressionMode.Compress))
//Process queue
while (dataQueue.Count > 0)
{
dstream.Write(rawData, 0, rawData.Length);
byte b = 0;
for (int i = 7; i >= 0; i--)
{
b += (byte)(dataQueue.Dequeue() << i);
}
targetStream.WriteByte(b);
}
rawData = output.ToArray();
}
else if (compressMode == Compression.GZip)
finally
{
using var output = new MemoryStream();
using (var gzipStream = new GZipStream(output, CompressionMode.Compress, true))
{
gzipStream.Write(rawData, 0, rawData.Length);
}
rawData = output.ToArray();
//Close compression streams to flush data
deflateStream?.Dispose();
gzipStream?.Dispose();
}
return rawData;

return output.ToArray();
}

/// <summary>
Expand Down
42 changes: 42 additions & 0 deletions QRCoderTests/QRGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,48 @@ public SamplePayload(string data, QRCodeGenerator.ECCLevel eccLevel)

public override string ToString() => _data;
}

[Theory]
[InlineData(QRCodeData.Compression.Uncompressed)]
[InlineData(QRCodeData.Compression.Deflate)]
[InlineData(QRCodeData.Compression.GZip)]
public void can_save_and_load_qrcode_data(QRCodeData.Compression compressionMode)
{
// Arrange - Create a QR code
var originalQrData = QRCodeGenerator.GenerateQrCode("https://github.com/Shane32/QRCoder", ECCLevel.H);
var originalMatrix = string.Join("", originalQrData.ModuleMatrix.Select(x => x.ToBitString()).ToArray());

// Act - Get raw data and reload it
var rawData = originalQrData.GetRawData(compressionMode);
var reloadedQrData = new QRCodeData(rawData, compressionMode);
var reloadedMatrix = string.Join("", reloadedQrData.ModuleMatrix.Select(x => x.ToBitString()).ToArray());

// Assert - Verify the data matches
reloadedQrData.Version.ShouldBe(originalQrData.Version);
reloadedQrData.ModuleMatrix.Count.ShouldBe(originalQrData.ModuleMatrix.Count);
reloadedMatrix.ShouldBe(originalMatrix);
}

[Theory]
[InlineData(QRCodeData.Compression.Uncompressed)]
[InlineData(QRCodeData.Compression.Deflate)]
[InlineData(QRCodeData.Compression.GZip)]
public void can_save_and_load_micro_qrcode_data(QRCodeData.Compression compressionMode)
{
// Arrange - Create a QR code
var originalQrData = QRCodeGenerator.GenerateMicroQrCode("abcd");
var originalMatrix = string.Join("", originalQrData.ModuleMatrix.Select(x => x.ToBitString()).ToArray());

// Act - Get raw data and reload it
var rawData = originalQrData.GetRawData(compressionMode);
var reloadedQrData = new QRCodeData(rawData, compressionMode);
var reloadedMatrix = string.Join("", reloadedQrData.ModuleMatrix.Select(x => x.ToBitString()).ToArray());

// Assert - Verify the data matches
reloadedQrData.Version.ShouldBe(originalQrData.Version);
reloadedQrData.ModuleMatrix.Count.ShouldBe(originalQrData.ModuleMatrix.Count);
reloadedMatrix.ShouldBe(originalMatrix);
}
}

public static class ExtensionMethods
Expand Down