diff --git a/QRCoder/PayloadGenerator.cs b/QRCoder/PayloadGenerator.cs index 39bae5f0..b4b0fd76 100644 --- a/QRCoder/PayloadGenerator.cs +++ b/QRCoder/PayloadGenerator.cs @@ -15,7 +15,7 @@ public static class PayloadGenerator public abstract class Payload { public virtual int Version { get { return -1; } } - public virtual QRCodeGenerator.ECCLevel EccLevel { get { return QRCodeGenerator.ECCLevel.M; } } + public virtual QRCodeGenerator.ECCLevel EccLevel { get { return QRCodeGenerator.ECCLevel.Default; } } public virtual QRCodeGenerator.EciMode EciMode { get { return QRCodeGenerator.EciMode.Default; } } public abstract override string ToString(); } diff --git a/QRCoder/QRCodeGenerator.ECCLevel.cs b/QRCoder/QRCodeGenerator.ECCLevel.cs index aedfa3c9..d8a0ad57 100644 --- a/QRCoder/QRCodeGenerator.ECCLevel.cs +++ b/QRCoder/QRCodeGenerator.ECCLevel.cs @@ -8,29 +8,35 @@ public partial class QRCodeGenerator /// public enum ECCLevel { + /// + /// Default error correction level, which will select Level M (Medium) unless otherwise specified by the payload. + /// Level M allows approximately 15% of data to be recovered, offering a balance between data capacity and error recovery. + /// + Default = -1, + /// /// Level L: Low error correction (approximately 7% of data can be recovered). /// This level allows the highest data density. /// - L, + L = 0, /// /// Level M: Medium error correction (approximately 15% of data can be recovered). /// Offers a balance between data capacity and error recovery. /// - M, + M = 1, /// /// Level Q: Quartile error correction (approximately 25% of data can be recovered). /// More robust error correction at the cost of reduced data capacity. /// - Q, + Q = 2, /// /// Level H: High error correction (approximately 30% of data can be recovered). /// Provides the highest level of error recovery, ideal for environments with high risk of data loss. /// - H + H = 3 } } } diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index 43a69768..7f52921a 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -105,6 +105,10 @@ public static QRCodeData GenerateQrCode(PayloadGenerator.Payload payload) /// Returns the raw QR code data which can be used for rendering. public static QRCodeData GenerateQrCode(PayloadGenerator.Payload payload, ECCLevel eccLevel) { + if (eccLevel == ECCLevel.Default) + eccLevel = payload.EccLevel; + else if (payload.EccLevel != ECCLevel.Default && eccLevel != payload.EccLevel) + throw new ArgumentOutOfRangeException(nameof(eccLevel), $"The provided payload requires a ECC level of {eccLevel}."); return GenerateQrCode(payload.ToString(), eccLevel, false, false, payload.EciMode, payload.Version); } @@ -121,6 +125,7 @@ public static QRCodeData GenerateQrCode(PayloadGenerator.Payload payload, ECCLev /// Returns the raw QR code data which can be used for rendering. public static QRCodeData GenerateQrCode(string plainText, ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, EciMode eciMode = EciMode.Default, int requestedVersion = -1) { + eccLevel = ValidateECCLevel(eccLevel); EncodingMode encoding = GetEncodingFromPlaintext(plainText, forceUtf8); var codedText = PlainTextToBinary(plainText, encoding, eciMode, utf8BOM, forceUtf8); var dataInputLength = GetDataLength(encoding, plainText, codedText, forceUtf8); @@ -165,7 +170,6 @@ public static QRCodeData GenerateQrCode(string plainText, ECCLevel eccLevel, boo return GenerateQrCode(completeBitArray, eccLevel, version); } - /// /// Calculates the QR code data which than can be used in one of the rendering classes to generate a graphical representation. /// @@ -175,6 +179,7 @@ public static QRCodeData GenerateQrCode(string plainText, ECCLevel eccLevel, boo /// Returns the raw QR code data which can be used for rendering. public static QRCodeData GenerateQrCode(byte[] binaryData, ECCLevel eccLevel) { + eccLevel = ValidateECCLevel(eccLevel); int version = GetVersion(binaryData.Length, EncodingMode.Byte, eccLevel); int countIndicatorLen = GetCountIndicatorLength(version, EncodingMode.Byte); @@ -187,6 +192,27 @@ public static QRCodeData GenerateQrCode(byte[] binaryData, ECCLevel eccLevel) return GenerateQrCode(bitArray, eccLevel, version); } + /// + /// Validates the specified error correction level. + /// Returns the provided level if it is valid, or the level M if the provided level is Default. + /// Throws an exception if an invalid level is provided. + /// + private static ECCLevel ValidateECCLevel(ECCLevel eccLevel) + { + switch (eccLevel) + { + case ECCLevel.L: + case ECCLevel.M: + case ECCLevel.Q: + case ECCLevel.H: + return eccLevel; + case ECCLevel.Default: + return ECCLevel.M; + default: + throw new ArgumentOutOfRangeException(nameof(eccLevel), eccLevel, "Invalid error correction level."); + } + } + private static readonly BitArray _repeatingPattern = new BitArray( new[] { true, true, true, false, true, true, false, false, false, false, false, true, false, false, false, true }); @@ -371,7 +397,7 @@ void WriteEccLevelAndVersion() #endif private static void TrimLeadingZeros(BitArray fStrEcc, ref int index, ref int count) { - while (!fStrEcc[index]) + while (count > 0 && !fStrEcc[index]) { index++; count--; diff --git a/QRCoderApiTests/net35+net40+net50+net50-windows+netstandard20/QRCoder.approved.txt b/QRCoderApiTests/net35+net40+net50+net50-windows+netstandard20/QRCoder.approved.txt index 0573589b..60b1ecc2 100644 --- a/QRCoderApiTests/net35+net40+net50+net50-windows+netstandard20/QRCoder.approved.txt +++ b/QRCoderApiTests/net35+net40+net50+net50-windows+netstandard20/QRCoder.approved.txt @@ -895,6 +895,7 @@ namespace QRCoder public static QRCoder.QRCodeData GenerateQrCode(string plainText, QRCoder.QRCodeGenerator.ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, QRCoder.QRCodeGenerator.EciMode eciMode = 0, int requestedVersion = -1) { } public enum ECCLevel { + Default = -1, L = 0, M = 1, Q = 2, diff --git a/QRCoderApiTests/net60-windows/QRCoder.approved.txt b/QRCoderApiTests/net60-windows/QRCoder.approved.txt index 3f03e15b..ff07d0df 100644 --- a/QRCoderApiTests/net60-windows/QRCoder.approved.txt +++ b/QRCoderApiTests/net60-windows/QRCoder.approved.txt @@ -903,6 +903,7 @@ namespace QRCoder public static QRCoder.QRCodeData GenerateQrCode(string plainText, QRCoder.QRCodeGenerator.ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, QRCoder.QRCodeGenerator.EciMode eciMode = 0, int requestedVersion = -1) { } public enum ECCLevel { + Default = -1, L = 0, M = 1, Q = 2, diff --git a/QRCoderApiTests/net60/QRCoder.approved.txt b/QRCoderApiTests/net60/QRCoder.approved.txt index 7b3c4ef1..e558ea6f 100644 --- a/QRCoderApiTests/net60/QRCoder.approved.txt +++ b/QRCoderApiTests/net60/QRCoder.approved.txt @@ -837,6 +837,7 @@ namespace QRCoder public static QRCoder.QRCodeData GenerateQrCode(string plainText, QRCoder.QRCodeGenerator.ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, QRCoder.QRCodeGenerator.EciMode eciMode = 0, int requestedVersion = -1) { } public enum ECCLevel { + Default = -1, L = 0, M = 1, Q = 2, diff --git a/QRCoderApiTests/netstandard13/QRCoder.approved.txt b/QRCoderApiTests/netstandard13/QRCoder.approved.txt index 311ba68f..34362e64 100644 --- a/QRCoderApiTests/netstandard13/QRCoder.approved.txt +++ b/QRCoderApiTests/netstandard13/QRCoder.approved.txt @@ -802,6 +802,7 @@ namespace QRCoder public static QRCoder.QRCodeData GenerateQrCode(string plainText, QRCoder.QRCodeGenerator.ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, QRCoder.QRCodeGenerator.EciMode eciMode = 0, int requestedVersion = -1) { } public enum ECCLevel { + Default = -1, L = 0, M = 1, Q = 2, diff --git a/QRCoderTests/QRGeneratorTests.cs b/QRCoderTests/QRGeneratorTests.cs index 8d0818cd..87b61fec 100644 --- a/QRCoderTests/QRGeneratorTests.cs +++ b/QRCoderTests/QRGeneratorTests.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Collections; using System.Text; +using System; namespace QRCoderTests { @@ -191,6 +192,16 @@ public void can_generate_from_bytes() result.ShouldBe("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111011001011111110000000010000010010010100000100000000101110101010101011101000000001011101010010010111010000000010111010111000101110100000000100000100000001000001000000001111111010101011111110000000000000000011000000000000000000111100101010010011101000000001011100001001001001110000000010101011111011111110100000000000101000000110000000000000001011001001010100110000000000000000000110001000101000000000111111100110011011110000000001000001001111110111010000000010111010011100100101100000000101110101110010010010000000001011101011010100011000000000010000010110110101000100000000111111101011100010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); } + [Fact] + [Category("QRGenerator/TextEncoding")] + public void trim_leading_zeros_works() + { + var gen = new QRCodeGenerator(); + var qrData = gen.CreateQrCode("this is a test", QRCodeGenerator.ECCLevel.M); + var result = string.Join("", qrData.ModuleMatrix.Select(x => x.ToBitString()).ToArray()); + result.ShouldBe("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111001101011111110000000010000010010000100000100000000101110101101101011101000000001011101010001010111010000000010111010101010101110100000000100000101010101000001000000001111111010101011111110000000000000000110010000000000000000101111100011101111100000000001110100011110001100010000000001100011010110010011000000000100111000011010011100000000001001011001101011000100000000000000000100100001001100000000111111100111110001110000000001000001010011000011010000000010111010101110111101100000000101110101000000110100000000001011101011111000010000000000010000010010011010010000000000111111101101111100010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + } + [Fact] [Category("QRGenerator/TextEncoding")] public void isValidIso_works() @@ -216,6 +227,78 @@ bool IsValidISO(string input) } } } + + [Fact] + [Category("QRGenerator/EccLevel")] + public void ecc_level_from_payload_works() + { + var stringValue = "this is a test"; + + // set up baselines + var expectedL = Encode(QRCodeGenerator.GenerateQrCode(stringValue, QRCodeGenerator.ECCLevel.L)); + var expectedM = Encode(QRCodeGenerator.GenerateQrCode(stringValue, QRCodeGenerator.ECCLevel.M)); + var expectedQ = Encode(QRCodeGenerator.GenerateQrCode(stringValue, QRCodeGenerator.ECCLevel.Q)); + var expectedH = Encode(QRCodeGenerator.GenerateQrCode(stringValue, QRCodeGenerator.ECCLevel.H)); + + // ensure that the baselines are different from each other + expectedL.ShouldNotBe(expectedM); + expectedL.ShouldNotBe(expectedQ); + expectedL.ShouldNotBe(expectedH); + expectedM.ShouldNotBe(expectedQ); + expectedM.ShouldNotBe(expectedH); + expectedQ.ShouldNotBe(expectedH); + + // validate that any ECC level can be used when the payload specifies a default ECC level + var payloadDefault = new SamplePayload(stringValue, QRCodeGenerator.ECCLevel.Default); + Encode(QRCodeGenerator.GenerateQrCode(payloadDefault)).ShouldBe(expectedM); + Encode(QRCodeGenerator.GenerateQrCode(payloadDefault, QRCodeGenerator.ECCLevel.Default)).ShouldBe(expectedM); + Encode(QRCodeGenerator.GenerateQrCode(payloadDefault, QRCodeGenerator.ECCLevel.L)).ShouldBe(expectedL); + Encode(QRCodeGenerator.GenerateQrCode(payloadDefault, QRCodeGenerator.ECCLevel.M)).ShouldBe(expectedM); + Encode(QRCodeGenerator.GenerateQrCode(payloadDefault, QRCodeGenerator.ECCLevel.Q)).ShouldBe(expectedQ); + Encode(QRCodeGenerator.GenerateQrCode(payloadDefault, QRCodeGenerator.ECCLevel.H)).ShouldBe(expectedH); + + // validate that the ECC level specified in the payload is used when default is specified, + // or checks that the selected ECC level matches the payload ECC level, throwing an exception otherwise + Verify(QRCodeGenerator.ECCLevel.L, expectedL); + Verify(QRCodeGenerator.ECCLevel.M, expectedM); + Verify(QRCodeGenerator.ECCLevel.Q, expectedQ); + Verify(QRCodeGenerator.ECCLevel.H, expectedH); + + + void Verify(QRCodeGenerator.ECCLevel eccLevel, string expected) + { + var payload = new SamplePayload(stringValue, eccLevel); + Encode(QRCodeGenerator.GenerateQrCode(payload)).ShouldBe(expected); + foreach (var ecc in Enum.GetValues(typeof(QRCodeGenerator.ECCLevel)).Cast()) + { + if (ecc == eccLevel || ecc == QRCodeGenerator.ECCLevel.Default) + Encode(QRCodeGenerator.GenerateQrCode(payload, ecc)).ShouldBe(expected); + else + Should.Throw(() => Encode(QRCodeGenerator.GenerateQrCode(payload, ecc))); + } + } + + string Encode(QRCodeData qrData) + { + return string.Join("", qrData.ModuleMatrix.Select(x => x.ToBitString()).ToArray()); + } + } + + private class SamplePayload : PayloadGenerator.Payload + { + private string _data; + private QRCodeGenerator.ECCLevel _eccLevel; + + public SamplePayload(string data, QRCodeGenerator.ECCLevel eccLevel) + { + _data = data; + _eccLevel = eccLevel; + } + + public override QRCodeGenerator.ECCLevel EccLevel => _eccLevel; + + public override string ToString() => _data; + } } public static class ExtensionMethods