From e97301b9a49c0a1992ac73094d69922e7a28cf89 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Tue, 7 May 2024 10:37:19 -0400 Subject: [PATCH 1/8] Add string optimizations --- QRCoder/QRCodeGenerator.cs | 86 ++++++++++++++++++++++++-------- QRCoderTests/QRGeneratorTests.cs | 44 ++++++++++++++++ 2 files changed, 108 insertions(+), 22 deletions(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index 9716017d..bce86190 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -745,14 +745,21 @@ bool IsUtf8() } } + private static readonly Encoding _iso88591ExceptionFallback = Encoding.GetEncoding("ISO-8859-1", new EncoderExceptionFallback(), new DecoderExceptionFallback()); /// /// Checks if the given string can be accurately represented and retrieved in ISO-8859-1 encoding. /// private static bool IsValidISO(string input) { - var bytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(input); - var result = Encoding.GetEncoding("ISO-8859-1").GetString(bytes); - return String.Equals(input, result); + try + { + _ = _iso88591ExceptionFallback.GetByteCount(input); + return true; + } + catch (EncoderFallbackException) + { + return false; + } } /// @@ -866,18 +873,13 @@ private static BitArray PlainTextToBinaryAlphanumeric(string plainText) return codeText; } - /// - /// Returns a string that contains the original string, with characters that cannot be encoded by a - /// specified encoding (default of ISO-8859-2) with a replacement character. - /// - private static string ConvertToIso8859(string value, string Iso = "ISO-8859-2") - { - Encoding iso = Encoding.GetEncoding(Iso); - Encoding utf8 = Encoding.UTF8; - byte[] utfBytes = utf8.GetBytes(value); - byte[] isoBytes = Encoding.Convert(utf8, iso, utfBytes); - return iso.GetString(isoBytes); - } + private static readonly Encoding _iso8859_1 = +#if NET5_0_OR_GREATER + Encoding.Latin1; +#else + Encoding.GetEncoding("ISO-8859-1"); +#endif + private static Encoding _iso8859_2; /// /// Converts plain text into a binary format using byte mode encoding, which supports various character encodings through ECI (Extended Channel Interpretations). @@ -894,11 +896,14 @@ private static string ConvertToIso8859(string value, string Iso = "ISO-8859-2") /// private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, bool utf8BOM, bool forceUtf8) { - byte[] codeBytes; + Encoding targetEncoding; // Check if the text is valid ISO-8859-1 and UTF-8 is not forced, then encode using ISO-8859-1. if (IsValidISO(plainText) && !forceUtf8) - codeBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(plainText); + { + targetEncoding = _iso8859_1; + utf8BOM = false; + } else { // Determine the encoding based on the specified ECI mode. @@ -906,23 +911,54 @@ private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, { case EciMode.Iso8859_1: // Convert text to ISO-8859-1 and encode. - codeBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(ConvertToIso8859(plainText, "ISO-8859-1")); + targetEncoding = _iso8859_1; + utf8BOM = false; break; case EciMode.Iso8859_2: + // Note: ISO-8859-2 is not natively supported on .NET Core + // + // Users must install the System.Text.Encoding.CodePages package and call Encoding.RegisterProvider(CodePagesEncodingProvider.Instance) + // before using this encoding mode. + if (_iso8859_2 == null) + _iso8859_2 = Encoding.GetEncoding("ISO-8859-2"); // Convert text to ISO-8859-2 and encode. - codeBytes = Encoding.GetEncoding("ISO-8859-2").GetBytes(ConvertToIso8859(plainText, "ISO-8859-2")); + targetEncoding = _iso8859_2; + utf8BOM = false; break; case EciMode.Default: case EciMode.Utf8: default: // Handle UTF-8 encoding, optionally adding a BOM if specified. - codeBytes = utf8BOM ? Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(plainText)).ToArray() : Encoding.UTF8.GetBytes(plainText); + targetEncoding = Encoding.UTF8; break; } } +#if NET5_0_OR_GREATER + // In .NET 5.0 and later, we can use stackalloc for small arrays to prevent heap allocations + int count = targetEncoding.GetByteCount(plainText); + Span codeBytes = count < 2000 ? stackalloc byte[count] : new byte[count]; + targetEncoding.GetBytes(plainText, codeBytes); +#else + byte[] codeBytes; + codeBytes = targetEncoding.GetBytes(plainText); +#endif + // Convert the array of bytes into a BitArray. - return ToBitArray(codeBytes); + if (utf8BOM) + { + // convert to bit array, leaving 24 bits for the UTF-8 preamble + var bitArray = ToBitArray(codeBytes, 24); + // write UTF8 preamble (EF BB BF) to the BitArray + DecToBin(0xEF, 8, bitArray, 0); + DecToBin(0xBB, 8, bitArray, 8); + DecToBin(0xBF, 8, bitArray, 16); + return bitArray; + } + else + { + return ToBitArray(codeBytes); + } } /// @@ -932,7 +968,13 @@ private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, /// The byte array to convert into a BitArray. /// The number of leading zeros to prepend to the resulting BitArray. /// A BitArray representing the bits of the input byteArray, with optional leading zeros. - private static BitArray ToBitArray(byte[] byteArray, int prefixZeros = 0) + private static BitArray ToBitArray( +#if NET5_0_OR_GREATER + ReadOnlySpan byteArray, +#else + byte[] byteArray, +#endif + int prefixZeros = 0) { // Calculate the total number of bits in the resulting BitArray including the prefix zeros. var bitArray = new BitArray((int)((uint)byteArray.Length * 8) + prefixZeros); diff --git a/QRCoderTests/QRGeneratorTests.cs b/QRCoderTests/QRGeneratorTests.cs index 49872334..17bd2e9e 100644 --- a/QRCoderTests/QRGeneratorTests.cs +++ b/QRCoderTests/QRGeneratorTests.cs @@ -160,6 +160,26 @@ public void can_encode_byte() result.ShouldBe("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111001011011111110000000010000010011100100000100000000101110101101101011101000000001011101001010010111010000000010111010001010101110100000000100000100000101000001000000001111111010101011111110000000000000000110110000000000000000111011111111011000100000000001001110001100010000010000000010011110001010001001000000000110011010000001000110000000001110001111001010110110000000000000000111101010011100000000111111101111011100110000000001000001010011101110010000000010111010110101110010100000000101110100110001000110000000001011101011001000100010000000010000010100000100011000000000111111101110101010111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); } + [Fact] + [Category("QRGenerator/TextEncoding")] + public void can_encode_utf8() + { + var gen = new QRCodeGenerator(); + var qrData = gen.CreateQrCode("https://en.wikipedia.org/wiki/🍕", QRCodeGenerator.ECCLevel.L, true, false, QRCodeGenerator.EciMode.Utf8); + var result = string.Join("", qrData.ModuleMatrix.Select(x => x.ToBitString()).ToArray()); + result.ShouldBe("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111110101011010011101111111000000001000001001111100001110100000100000000101110101110000011000010111010000000010111010111010111100101011101000000001011101010011010111010101110100000000100000100011010001110010000010000000011111110101010101010101111111000000000000000000101000011100000000000000000111100101011110101011100111010000000001011000101011111010011101010000000001010011101111101001111011101000000000111011011110000010001100000100000000000000010011010101100000000000000000001100110101011011111001101110000000000000011100001010101010110101000000000000111001011100110111111110011000000001110101011001011001000100011000000000000101010100001010111111000000000000010111010101001111100000001110000000000010110100010111111100100010100000000011101111010011101111111101010000000000000000110000001000100010010000000001111111001100011001010101101000000000100000100111111111011000111000000000010111010010100011010111110111000000001011101010110100011100101011000000000101110101100101111100101111010000000010000010111011001111000001101000000001111111011110000100000110101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + } + + [Fact] + [Category("QRGenerator/TextEncoding")] + public void can_encode_utf8_bom() + { + var gen = new QRCodeGenerator(); + var qrData = gen.CreateQrCode("https://en.wikipedia.org/wiki/🍕", QRCodeGenerator.ECCLevel.L, true, true, QRCodeGenerator.EciMode.Utf8); + var result = string.Join("", qrData.ModuleMatrix.Select(x => x.ToBitString()).ToArray()); + result.ShouldBe("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111110010001101010101111111000000001000001011011000110000100000100000000101110100111010101111010111010000000010111010110100100010101011101000000001011101000101111000010101110100000000100000101010000111000010000010000000011111110101010101010101111111000000000000000000001010101110000000000000000111110111110101010100101010100000000000100000110000101000001100101000000000001001001011000011010000111100000000100010001111000001111110111010000000010110111010100011100100101111000000000001010001101101001000010100100000000100001101110011001010000001010000000001011001100011001111111010111000000000010001010101011110010100000100000000100100010000000000010110010000000000010110110010110000101010101100000000001001100100010010100111101101100000000101010110011000111101111100100000000000000000111011110011100011010000000001111111011100110010010101110000000000100000100100110010101000110110000000010111010110010111101111110011000000001011101010100000100010110100000000000101110101001100111110110111100000000010000010111100101111100100001000000001111111011110001110100111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); + } + [Fact] [Category("QRGenerator/TextEncoding")] public void can_generate_from_bytes() @@ -170,6 +190,30 @@ public void can_generate_from_bytes() var result = string.Join("", qrData.ModuleMatrix.Select(x => x.ToBitString()).ToArray()); result.ShouldBe("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111011001011111110000000010000010010010100000100000000101110101010101011101000000001011101010010010111010000000010111010111000101110100000000100000100000001000001000000001111111010101011111110000000000000000011000000000000000000111100101010010011101000000001011100001001001001110000000010101011111011111110100000000000101000000110000000000000001011001001010100110000000000000000000110001000101000000000111111100110011011110000000001000001001111110111010000000010111010011100100101100000000101110101110010010010000000001011101011010100011000000000010000010110110101000100000000111111101011100010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"); } + + [Fact] + [Category("QRGenerator/TextEncoding")] + public void isValidIso_works() + { + Encoding _iso88591ExceptionFallback = Encoding.GetEncoding("ISO-8859-1", new EncoderExceptionFallback(), new DecoderExceptionFallback()); + + IsValidISO("abc").ShouldBeTrue(); + IsValidISO("äöü").ShouldBeTrue(); + IsValidISO("🍕").ShouldBeFalse(); + + bool IsValidISO(string input) + { + try + { + _ = _iso88591ExceptionFallback.GetByteCount(input); + return true; + } + catch (EncoderFallbackException) + { + return false; + } + } + } } public static class ExtensionMethods From fa926af4a488bcf524c5b111457ce5dd75aaa25d Mon Sep 17 00:00:00 2001 From: Shane32 Date: Tue, 7 May 2024 10:45:57 -0400 Subject: [PATCH 2/8] Use code page numbers --- QRCoder/QRCodeGenerator.cs | 6 +++--- QRCoderTests/QRGeneratorTests.cs | 4 +++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index bce86190..538ec996 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -745,7 +745,7 @@ bool IsUtf8() } } - private static readonly Encoding _iso88591ExceptionFallback = Encoding.GetEncoding("ISO-8859-1", new EncoderExceptionFallback(), new DecoderExceptionFallback()); + private static readonly Encoding _iso88591ExceptionFallback = Encoding.GetEncoding(28591, new EncoderExceptionFallback(), new DecoderExceptionFallback()); // ISO-8859-1 /// /// Checks if the given string can be accurately represented and retrieved in ISO-8859-1 encoding. /// @@ -877,7 +877,7 @@ private static BitArray PlainTextToBinaryAlphanumeric(string plainText) #if NET5_0_OR_GREATER Encoding.Latin1; #else - Encoding.GetEncoding("ISO-8859-1"); + Encoding.GetEncoding(28591); // ISO-8859-1 #endif private static Encoding _iso8859_2; @@ -920,7 +920,7 @@ private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, // Users must install the System.Text.Encoding.CodePages package and call Encoding.RegisterProvider(CodePagesEncodingProvider.Instance) // before using this encoding mode. if (_iso8859_2 == null) - _iso8859_2 = Encoding.GetEncoding("ISO-8859-2"); + _iso8859_2 = Encoding.GetEncoding(28592); // ISO-8859-2 // Convert text to ISO-8859-2 and encode. targetEncoding = _iso8859_2; utf8BOM = false; diff --git a/QRCoderTests/QRGeneratorTests.cs b/QRCoderTests/QRGeneratorTests.cs index 17bd2e9e..8d0818cd 100644 --- a/QRCoderTests/QRGeneratorTests.cs +++ b/QRCoderTests/QRGeneratorTests.cs @@ -195,7 +195,9 @@ public void can_generate_from_bytes() [Category("QRGenerator/TextEncoding")] public void isValidIso_works() { - Encoding _iso88591ExceptionFallback = Encoding.GetEncoding("ISO-8859-1", new EncoderExceptionFallback(), new DecoderExceptionFallback()); + // see private method: QRCodeGenerator.IsValidISO + + Encoding _iso88591ExceptionFallback = Encoding.GetEncoding(28591, new EncoderExceptionFallback(), new DecoderExceptionFallback()); // ISO-8859-1 IsValidISO("abc").ShouldBeTrue(); IsValidISO("äöü").ShouldBeTrue(); From 27f29c3ed5c0f75c78ed820e8d5fd9358d86042d Mon Sep 17 00:00:00 2001 From: Shane32 Date: Tue, 7 May 2024 10:52:54 -0400 Subject: [PATCH 3/8] Update --- QRCoder/QRCodeGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index 538ec996..cfa9e78d 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -937,7 +937,7 @@ private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, #if NET5_0_OR_GREATER // In .NET 5.0 and later, we can use stackalloc for small arrays to prevent heap allocations int count = targetEncoding.GetByteCount(plainText); - Span codeBytes = count < 2000 ? stackalloc byte[count] : new byte[count]; + Span codeBytes = count < 3000 ? stackalloc byte[count] : new byte[count]; targetEncoding.GetBytes(plainText, codeBytes); #else byte[] codeBytes; From e08d6e3a63e0ad3fdce26d023b355199e4a0676e Mon Sep 17 00:00:00 2001 From: Shane32 Date: Tue, 7 May 2024 11:00:14 -0400 Subject: [PATCH 4/8] Update comments --- QRCoder/QRCodeGenerator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index cfa9e78d..b4eb63bc 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -934,8 +934,10 @@ private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, } } -#if NET5_0_OR_GREATER - // In .NET 5.0 and later, we can use stackalloc for small arrays to prevent heap allocations +#if NETCOREAPP2_1_OR_GREATER + // We can use stackalloc for small arrays to prevent heap allocations + // (also available in .NET Standard and .NET Framework with an additional NuGet dependency) + // Note that all QR codes should fit within 3000 bytes, so this code should never trigger a heap allocation unless an exception will be thrown anyway. int count = targetEncoding.GetByteCount(plainText); Span codeBytes = count < 3000 ? stackalloc byte[count] : new byte[count]; targetEncoding.GetBytes(plainText, codeBytes); From cd06130b381cc0d4d6e0efc0e6c3df49329ccdc2 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Tue, 7 May 2024 11:02:56 -0400 Subject: [PATCH 5/8] Add comment --- QRCoder/QRCodeGenerator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index b4eb63bc..e004a570 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -751,12 +751,13 @@ bool IsUtf8() /// private static bool IsValidISO(string input) { + // No heap allocations if the string is ISO-8859-1 try { _ = _iso88591ExceptionFallback.GetByteCount(input); return true; } - catch (EncoderFallbackException) + catch (EncoderFallbackException) // The exception is a heap allocation and not ideal { return false; } From 554c527a185863ae4c0604920d4223811cae0e28 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Tue, 7 May 2024 11:05:47 -0400 Subject: [PATCH 6/8] Add comments --- QRCoder/QRCodeGenerator.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index e004a570..7ca8f5d4 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -943,8 +943,7 @@ private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, Span codeBytes = count < 3000 ? stackalloc byte[count] : new byte[count]; targetEncoding.GetBytes(plainText, codeBytes); #else - byte[] codeBytes; - codeBytes = targetEncoding.GetBytes(plainText); + byte[] codeBytes = targetEncoding.GetBytes(plainText); #endif // Convert the array of bytes into a BitArray. @@ -973,7 +972,7 @@ private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, /// A BitArray representing the bits of the input byteArray, with optional leading zeros. private static BitArray ToBitArray( #if NET5_0_OR_GREATER - ReadOnlySpan byteArray, + ReadOnlySpan byteArray, // byte[] has an implicit cast to ReadOnlySpan #else byte[] byteArray, #endif From cb5a49747a7d85a3289254286e8ad76162a395e2 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Tue, 7 May 2024 11:18:05 -0400 Subject: [PATCH 7/8] Update ifdef --- QRCoder/QRCodeGenerator.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index 7ca8f5d4..8f63fb26 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -935,9 +935,8 @@ private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, } } -#if NETCOREAPP2_1_OR_GREATER +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1 // We can use stackalloc for small arrays to prevent heap allocations - // (also available in .NET Standard and .NET Framework with an additional NuGet dependency) // Note that all QR codes should fit within 3000 bytes, so this code should never trigger a heap allocation unless an exception will be thrown anyway. int count = targetEncoding.GetByteCount(plainText); Span codeBytes = count < 3000 ? stackalloc byte[count] : new byte[count]; @@ -971,7 +970,7 @@ private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, /// The number of leading zeros to prepend to the resulting BitArray. /// A BitArray representing the bits of the input byteArray, with optional leading zeros. private static BitArray ToBitArray( -#if NET5_0_OR_GREATER +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1 ReadOnlySpan byteArray, // byte[] has an implicit cast to ReadOnlySpan #else byte[] byteArray, From 5037a5a3b0cd0ffe3482b80b12160feaadcd8d29 Mon Sep 17 00:00:00 2001 From: Shane32 Date: Sat, 11 May 2024 11:05:58 -0400 Subject: [PATCH 8/8] Utilize suggestions --- QRCoder/QRCodeGenerator.cs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index 8f63fb26..a5a75185 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -1,4 +1,7 @@ using System; +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1 +using System.Buffers; +#endif using System.Collections; using System.Collections.Generic; using System.Globalization; @@ -937,29 +940,41 @@ private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1 // We can use stackalloc for small arrays to prevent heap allocations - // Note that all QR codes should fit within 3000 bytes, so this code should never trigger a heap allocation unless an exception will be thrown anyway. + const int MAX_STACK_SIZE_IN_BYTES = 512; + int count = targetEncoding.GetByteCount(plainText); - Span codeBytes = count < 3000 ? stackalloc byte[count] : new byte[count]; + byte[] bufferFromPool = null; + Span codeBytes = (count <= MAX_STACK_SIZE_IN_BYTES) + ? (stackalloc byte[MAX_STACK_SIZE_IN_BYTES]) + : (bufferFromPool = ArrayPool.Shared.Rent(count)); + codeBytes = codeBytes.Slice(0, count); targetEncoding.GetBytes(plainText, codeBytes); #else byte[] codeBytes = targetEncoding.GetBytes(plainText); #endif // Convert the array of bytes into a BitArray. + BitArray bitArray; if (utf8BOM) { // convert to bit array, leaving 24 bits for the UTF-8 preamble - var bitArray = ToBitArray(codeBytes, 24); + bitArray = ToBitArray(codeBytes, 24); // write UTF8 preamble (EF BB BF) to the BitArray DecToBin(0xEF, 8, bitArray, 0); DecToBin(0xBB, 8, bitArray, 8); DecToBin(0xBF, 8, bitArray, 16); - return bitArray; } else { - return ToBitArray(codeBytes); + bitArray = ToBitArray(codeBytes); } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1 + if (bufferFromPool != null) + ArrayPool.Shared.Return(bufferFromPool); +#endif + + return bitArray; } ///