From 6839f2d85ca921f0a0bcb44cb86ce18138742065 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 1 Oct 2025 10:20:47 -0400 Subject: [PATCH 1/5] Optimize compression/decompression and add tests --- QRCoder/QRCodeData.cs | 107 ++++++++++++++++--------------- QRCoderTests/QRGeneratorTests.cs | 22 +++++++ 2 files changed, 78 insertions(+), 51 deletions(-) diff --git a/QRCoder/QRCodeData.cs b/QRCoder/QRCodeData.cs index 852159af..99ff25ec 100644 --- a/QRCoder/QRCodeData.cs +++ b/QRCoder/QRCodeData.cs @@ -55,46 +55,48 @@ public QRCodeData(string pathToRawData, Compression compressMode) : this(File.Re /// /// Initializes a new instance of the class with raw data and compression mode. /// - /// The raw data of the QR code. + /// The raw data of the QR code. /// The compression mode used for the raw data. - public QRCodeData(byte[] rawData, Compression compressMode) + public QRCodeData(byte[] bytes, Compression compressMode) { - var bytes = new List(rawData); - //Decompress if (compressMode == Compression.Deflate) { - using var input = new MemoryStream(bytes.ToArray()); + using var input = new MemoryStream(bytes); using var output = new MemoryStream(); using (var dstream = new DeflateStream(input, CompressionMode.Decompress)) { dstream.CopyTo(output); } - bytes = new List(output.ToArray()); + bytes = output.ToArray(); } else if (compressMode == Compression.GZip) { - using var input = new MemoryStream(bytes.ToArray()); + using var input = new MemoryStream(bytes); using var output = new MemoryStream(); using (var dstream = new GZipStream(input, CompressionMode.Decompress)) { dstream.CopyTo(output); } - bytes = new List(output.ToArray()); + bytes = output.ToArray(); } + var count = bytes.Length; + + if (count < 5) + throw new Exception("Invalid raw data file. File too short."); if (bytes[0] != 0x51 || bytes[1] != 0x52 || bytes[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; //Unpack - var modules = new Queue(8 * bytes.Count); - foreach (var b in bytes) + var modules = new Queue(8 * (count - 5)); + for (int j = 5; j < count; j++) { + var b = bytes[j]; for (int i = 7; i >= 0; i--) { modules.Enqueue((b & (1 << i)) != 0); @@ -111,7 +113,6 @@ public QRCodeData(byte[] rawData, Compression compressMode) ModuleMatrix[y][x] = modules.Dequeue(); } } - } /// @@ -121,60 +122,64 @@ public QRCodeData(byte[] rawData, Compression compressMode) /// Returns the raw data of the QR code as a byte array. public byte[] GetRawData(Compression compressMode) { - var bytes = new List(); + using var output = new MemoryStream(); + Stream targetStream = output; + DeflateStream? deflateStream = null; + GZipStream? gzipStream = null; - //Add header - signature ("QRR") - bytes.AddRange(new byte[] { 0x51, 0x52, 0x52, 0x00 }); - - //Add header - rowsize - bytes.Add((byte)ModuleMatrix.Count); - - //Build data queue - var dataQueue = new Queue(); - 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") + targetStream.Write(new byte[] { 0x51, 0x52, 0x52, 0x00 }, 0, 4); + + //Add header - rowsize + targetStream.WriteByte((byte)ModuleMatrix.Count); + + //Build data queue + var dataQueue = new Queue(); + foreach (var row in ModuleMatrix) { - b += (byte)(dataQueue.Dequeue() << i); + foreach (var module in row) + { + dataQueue.Enqueue((bool)module ? 1 : 0); + } + } + for (int i = 0; i < 8 - (ModuleMatrix.Count * ModuleMatrix.Count) % 8; 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(); } /// diff --git a/QRCoderTests/QRGeneratorTests.cs b/QRCoderTests/QRGeneratorTests.cs index 2f5921cd..d8274b7d 100644 --- a/QRCoderTests/QRGeneratorTests.cs +++ b/QRCoderTests/QRGeneratorTests.cs @@ -622,6 +622,28 @@ 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 gen = new QRCodeGenerator(); + var originalQrData = gen.CreateQrCode("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); + } } public static class ExtensionMethods From 518c84e653aac376291952f6b9926978bcc51f2e Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 1 Oct 2025 10:21:26 -0400 Subject: [PATCH 2/5] update --- QRCoderTests/QRGeneratorTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/QRCoderTests/QRGeneratorTests.cs b/QRCoderTests/QRGeneratorTests.cs index d8274b7d..78181f0b 100644 --- a/QRCoderTests/QRGeneratorTests.cs +++ b/QRCoderTests/QRGeneratorTests.cs @@ -623,6 +623,7 @@ public SamplePayload(string data, QRCodeGenerator.ECCLevel eccLevel) public override string ToString() => _data; } +#if !NETFRAMEWORK // [Theory] is not supported in xunit < 2.0.0 [Theory] [InlineData(QRCodeData.Compression.Uncompressed)] [InlineData(QRCodeData.Compression.Deflate)] @@ -644,6 +645,7 @@ public void can_save_and_load_qrcode_data(QRCodeData.Compression compressionMode reloadedQrData.ModuleMatrix.Count.ShouldBe(originalQrData.ModuleMatrix.Count); reloadedMatrix.ShouldBe(originalMatrix); } +#endif } public static class ExtensionMethods From b6afcf5af4a05be45e20ad44f9f454372e02a057 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 1 Oct 2025 11:33:47 -0400 Subject: [PATCH 3/5] update signature --- QRCoder/QRCodeData.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/QRCoder/QRCodeData.cs b/QRCoder/QRCodeData.cs index 99ff25ec..ee5f4332 100644 --- a/QRCoder/QRCodeData.cs +++ b/QRCoder/QRCodeData.cs @@ -55,48 +55,48 @@ public QRCodeData(string pathToRawData, Compression compressMode) : this(File.Re /// /// Initializes a new instance of the class with raw data and compression mode. /// - /// The raw data of the QR code. + /// The raw data of the QR code. /// The compression mode used for the raw data. - public QRCodeData(byte[] bytes, Compression compressMode) + public QRCodeData(byte[] rawData, Compression compressMode) { //Decompress if (compressMode == Compression.Deflate) { - using var input = new MemoryStream(bytes); + using var input = new MemoryStream(rawData); using var output = new MemoryStream(); using (var dstream = new DeflateStream(input, CompressionMode.Decompress)) { dstream.CopyTo(output); } - bytes = output.ToArray(); + rawData = output.ToArray(); } else if (compressMode == Compression.GZip) { - using var input = new MemoryStream(bytes); + using var input = new MemoryStream(rawData); using var output = new MemoryStream(); using (var dstream = new GZipStream(input, CompressionMode.Decompress)) { dstream.CopyTo(output); } - bytes = output.ToArray(); + rawData = output.ToArray(); } - var count = bytes.Length; + var count = rawData.Length; if (count < 5) throw new Exception("Invalid raw data file. File too short."); - if (bytes[0] != 0x51 || bytes[1] != 0x52 || bytes[2] != 0x52) + 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]; + var sideLen = (int)rawData[4]; Version = (sideLen - 21 - 8) / 4 + 1; //Unpack var modules = new Queue(8 * (count - 5)); for (int j = 5; j < count; j++) { - var b = bytes[j]; + var b = rawData[j]; for (int i = 7; i >= 0; i--) { modules.Enqueue((b & (1 << i)) != 0); From bf79ddfffdc88032d9751202f20d9fd8f33de48b Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 1 Oct 2025 13:43:44 -0400 Subject: [PATCH 4/5] Fix micro qr code reading --- QRCoder/QRCodeData.cs | 27 +++++++++++++++++++-------- QRCoderTests/QRGeneratorTests.cs | 26 ++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/QRCoder/QRCodeData.cs b/QRCoder/QRCodeData.cs index ee5f4332..42c3fbc6 100644 --- a/QRCoder/QRCodeData.cs +++ b/QRCoder/QRCodeData.cs @@ -81,20 +81,30 @@ public QRCodeData(byte[] rawData, Compression compressMode) rawData = output.ToArray(); } - var count = rawData.Length; - - if (count < 5) + 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 + // Set QR code version from side length (includes 8-module quiet zone) var sideLen = (int)rawData[4]; - Version = (sideLen - 21 - 8) / 4 + 1; + 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(8 * (count - 5)); - for (int j = 5; j < count; j++) + var modules = new Queue(8 * (rawData.Length - 5)); + for (int j = 5; j < rawData.Length; j++) { var b = rawData[j]; for (int i = 7; i >= 0; i--) @@ -156,7 +166,8 @@ public byte[] GetRawData(Compression compressMode) dataQueue.Enqueue((bool)module ? 1 : 0); } } - for (int i = 0; i < 8 - (ModuleMatrix.Count * ModuleMatrix.Count) % 8; i++) + int mod = (int)(((uint)ModuleMatrix.Count * (uint)ModuleMatrix.Count) % 8); + for (int i = 0; i < 8 - mod; i++) { dataQueue.Enqueue(0); } diff --git a/QRCoderTests/QRGeneratorTests.cs b/QRCoderTests/QRGeneratorTests.cs index 78181f0b..ded8cf89 100644 --- a/QRCoderTests/QRGeneratorTests.cs +++ b/QRCoderTests/QRGeneratorTests.cs @@ -623,7 +623,6 @@ public SamplePayload(string data, QRCodeGenerator.ECCLevel eccLevel) public override string ToString() => _data; } -#if !NETFRAMEWORK // [Theory] is not supported in xunit < 2.0.0 [Theory] [InlineData(QRCodeData.Compression.Uncompressed)] [InlineData(QRCodeData.Compression.Deflate)] @@ -631,8 +630,28 @@ public SamplePayload(string data, QRCodeGenerator.ECCLevel eccLevel) public void can_save_and_load_qrcode_data(QRCodeData.Compression compressionMode) { // Arrange - Create a QR code - var gen = new QRCodeGenerator(); - var originalQrData = gen.CreateQrCode("https://github.com/Shane32/QRCoder", ECCLevel.H); + 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 @@ -645,7 +664,6 @@ public void can_save_and_load_qrcode_data(QRCodeData.Compression compressionMode reloadedQrData.ModuleMatrix.Count.ShouldBe(originalQrData.ModuleMatrix.Count); reloadedMatrix.ShouldBe(originalMatrix); } -#endif } public static class ExtensionMethods From 46ca8714a1676719cd1189fdab62b82be1a94a79 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Wed, 1 Oct 2025 18:31:24 -0400 Subject: [PATCH 5/5] Update --- Directory.Build.props | 2 +- QRCoder/QRCodeData.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index b2ffd159..c391bd1d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 10 + 12 true $(WarningsNotAsErrors);IDE0005 false diff --git a/QRCoder/QRCodeData.cs b/QRCoder/QRCodeData.cs index ee5f4332..417f21d4 100644 --- a/QRCoder/QRCodeData.cs +++ b/QRCoder/QRCodeData.cs @@ -142,7 +142,11 @@ public byte[] GetRawData(Compression compressMode) try { //Add header - signature ("QRR") +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1 + targetStream.Write([0x51, 0x52, 0x52, 0x00]); +#else targetStream.Write(new byte[] { 0x51, 0x52, 0x52, 0x00 }, 0, 4); +#endif //Add header - rowsize targetStream.WriteByte((byte)ModuleMatrix.Count);