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 852159af..ffd2ca8b 100644 --- a/QRCoder/QRCodeData.cs +++ b/QRCoder/QRCodeData.cs @@ -59,42 +59,54 @@ public QRCodeData(string pathToRawData, Compression compressMode) : this(File.Re /// The compression mode used for the raw data. public QRCodeData(byte[] rawData, Compression compressMode) { - var bytes = new List(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(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(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(8 * bytes.Count); - foreach (var b in bytes) + 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--) { modules.Enqueue((b & (1 << i)) != 0); @@ -111,7 +123,6 @@ public QRCodeData(byte[] rawData, Compression compressMode) ModuleMatrix[y][x] = modules.Dequeue(); } } - } /// @@ -121,60 +132,69 @@ 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(); - - //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(); - 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]); +#else + targetStream.Write(new byte[] { 0x51, 0x52, 0x52, 0x00 }, 0, 4); +#endif + + //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); + } + } + 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(); } /// diff --git a/QRCoderTests/QRGeneratorTests.cs b/QRCoderTests/QRGeneratorTests.cs index 2f5921cd..ded8cf89 100644 --- a/QRCoderTests/QRGeneratorTests.cs +++ b/QRCoderTests/QRGeneratorTests.cs @@ -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