Skip to content

Commit 6d41db5

Browse files
authored
Merge pull request #521 from Shane32/string_optimizations
Add string optimizations
2 parents b26488e + 51e8c92 commit 6d41db5

File tree

2 files changed

+126
-22
lines changed

2 files changed

+126
-22
lines changed

QRCoder/QRCodeGenerator.cs

+80-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using System;
2+
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
3+
using System.Buffers;
4+
#endif
25
using System.Collections;
36
using System.Collections.Generic;
47
using System.Globalization;
@@ -711,14 +714,22 @@ bool IsUtf8()
711714
}
712715
}
713716

717+
private static readonly Encoding _iso88591ExceptionFallback = Encoding.GetEncoding(28591, new EncoderExceptionFallback(), new DecoderExceptionFallback()); // ISO-8859-1
714718
/// <summary>
715719
/// Checks if the given string can be accurately represented and retrieved in ISO-8859-1 encoding.
716720
/// </summary>
717721
private static bool IsValidISO(string input)
718722
{
719-
var bytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(input);
720-
var result = Encoding.GetEncoding("ISO-8859-1").GetString(bytes);
721-
return String.Equals(input, result);
723+
// No heap allocations if the string is ISO-8859-1
724+
try
725+
{
726+
_ = _iso88591ExceptionFallback.GetByteCount(input);
727+
return true;
728+
}
729+
catch (EncoderFallbackException) // The exception is a heap allocation and not ideal
730+
{
731+
return false;
732+
}
722733
}
723734

724735
/// <summary>
@@ -832,18 +843,13 @@ private static BitArray PlainTextToBinaryAlphanumeric(string plainText)
832843
return codeText;
833844
}
834845

835-
/// <summary>
836-
/// Returns a string that contains the original string, with characters that cannot be encoded by a
837-
/// specified encoding (default of ISO-8859-2) with a replacement character.
838-
/// </summary>
839-
private static string ConvertToIso8859(string value, string Iso = "ISO-8859-2")
840-
{
841-
Encoding iso = Encoding.GetEncoding(Iso);
842-
Encoding utf8 = Encoding.UTF8;
843-
byte[] utfBytes = utf8.GetBytes(value);
844-
byte[] isoBytes = Encoding.Convert(utf8, iso, utfBytes);
845-
return iso.GetString(isoBytes);
846-
}
846+
private static readonly Encoding _iso8859_1 =
847+
#if NET5_0_OR_GREATER
848+
Encoding.Latin1;
849+
#else
850+
Encoding.GetEncoding(28591); // ISO-8859-1
851+
#endif
852+
private static Encoding _iso8859_2;
847853

848854
/// <summary>
849855
/// Converts plain text into a binary format using byte mode encoding, which supports various character encodings through ECI (Extended Channel Interpretations).
@@ -860,35 +866,81 @@ private static string ConvertToIso8859(string value, string Iso = "ISO-8859-2")
860866
/// </remarks>
861867
private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, bool utf8BOM, bool forceUtf8)
862868
{
863-
byte[] codeBytes;
869+
Encoding targetEncoding;
864870

865871
// Check if the text is valid ISO-8859-1 and UTF-8 is not forced, then encode using ISO-8859-1.
866872
if (IsValidISO(plainText) && !forceUtf8)
867-
codeBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(plainText);
873+
{
874+
targetEncoding = _iso8859_1;
875+
utf8BOM = false;
876+
}
868877
else
869878
{
870879
// Determine the encoding based on the specified ECI mode.
871880
switch (eciMode)
872881
{
873882
case EciMode.Iso8859_1:
874883
// Convert text to ISO-8859-1 and encode.
875-
codeBytes = Encoding.GetEncoding("ISO-8859-1").GetBytes(ConvertToIso8859(plainText, "ISO-8859-1"));
884+
targetEncoding = _iso8859_1;
885+
utf8BOM = false;
876886
break;
877887
case EciMode.Iso8859_2:
888+
// Note: ISO-8859-2 is not natively supported on .NET Core
889+
//
890+
// Users must install the System.Text.Encoding.CodePages package and call Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
891+
// before using this encoding mode.
892+
if (_iso8859_2 == null)
893+
_iso8859_2 = Encoding.GetEncoding(28592); // ISO-8859-2
878894
// Convert text to ISO-8859-2 and encode.
879-
codeBytes = Encoding.GetEncoding("ISO-8859-2").GetBytes(ConvertToIso8859(plainText, "ISO-8859-2"));
895+
targetEncoding = _iso8859_2;
896+
utf8BOM = false;
880897
break;
881898
case EciMode.Default:
882899
case EciMode.Utf8:
883900
default:
884901
// Handle UTF-8 encoding, optionally adding a BOM if specified.
885-
codeBytes = utf8BOM ? Encoding.UTF8.GetPreamble().Concat(Encoding.UTF8.GetBytes(plainText)).ToArray() : Encoding.UTF8.GetBytes(plainText);
902+
targetEncoding = Encoding.UTF8;
886903
break;
887904
}
888905
}
889906

907+
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
908+
// We can use stackalloc for small arrays to prevent heap allocations
909+
const int MAX_STACK_SIZE_IN_BYTES = 512;
910+
911+
int count = targetEncoding.GetByteCount(plainText);
912+
byte[] bufferFromPool = null;
913+
Span<byte> codeBytes = (count <= MAX_STACK_SIZE_IN_BYTES)
914+
? (stackalloc byte[MAX_STACK_SIZE_IN_BYTES])
915+
: (bufferFromPool = ArrayPool<byte>.Shared.Rent(count));
916+
codeBytes = codeBytes.Slice(0, count);
917+
targetEncoding.GetBytes(plainText, codeBytes);
918+
#else
919+
byte[] codeBytes = targetEncoding.GetBytes(plainText);
920+
#endif
921+
890922
// Convert the array of bytes into a BitArray.
891-
return ToBitArray(codeBytes);
923+
BitArray bitArray;
924+
if (utf8BOM)
925+
{
926+
// convert to bit array, leaving 24 bits for the UTF-8 preamble
927+
bitArray = ToBitArray(codeBytes, 24);
928+
// write UTF8 preamble (EF BB BF) to the BitArray
929+
DecToBin(0xEF, 8, bitArray, 0);
930+
DecToBin(0xBB, 8, bitArray, 8);
931+
DecToBin(0xBF, 8, bitArray, 16);
932+
}
933+
else
934+
{
935+
bitArray = ToBitArray(codeBytes);
936+
}
937+
938+
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
939+
if (bufferFromPool != null)
940+
ArrayPool<byte>.Shared.Return(bufferFromPool);
941+
#endif
942+
943+
return bitArray;
892944
}
893945

894946
/// <summary>
@@ -898,7 +950,13 @@ private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode,
898950
/// <param name="byteArray">The byte array to convert into a BitArray.</param>
899951
/// <param name="prefixZeros">The number of leading zeros to prepend to the resulting BitArray.</param>
900952
/// <returns>A BitArray representing the bits of the input byteArray, with optional leading zeros.</returns>
901-
private static BitArray ToBitArray(byte[] byteArray, int prefixZeros = 0)
953+
private static BitArray ToBitArray(
954+
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1
955+
ReadOnlySpan<byte> byteArray, // byte[] has an implicit cast to ReadOnlySpan<byte>
956+
#else
957+
byte[] byteArray,
958+
#endif
959+
int prefixZeros = 0)
902960
{
903961
// Calculate the total number of bits in the resulting BitArray including the prefix zeros.
904962
var bitArray = new BitArray((int)((uint)byteArray.Length * 8) + prefixZeros);

QRCoderTests/QRGeneratorTests.cs

+46
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,26 @@ public void can_encode_byte()
160160
result.ShouldBe("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111001011011111110000000010000010011100100000100000000101110101101101011101000000001011101001010010111010000000010111010001010101110100000000100000100000101000001000000001111111010101011111110000000000000000110110000000000000000111011111111011000100000000001001110001100010000010000000010011110001010001001000000000110011010000001000110000000001110001111001010110110000000000000000111101010011100000000111111101111011100110000000001000001010011101110010000000010111010110101110010100000000101110100110001000110000000001011101011001000100010000000010000010100000100011000000000111111101110101010111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
161161
}
162162

163+
[Fact]
164+
[Category("QRGenerator/TextEncoding")]
165+
public void can_encode_utf8()
166+
{
167+
var gen = new QRCodeGenerator();
168+
var qrData = gen.CreateQrCode("https://en.wikipedia.org/wiki/🍕", QRCodeGenerator.ECCLevel.L, true, false, QRCodeGenerator.EciMode.Utf8);
169+
var result = string.Join("", qrData.ModuleMatrix.Select(x => x.ToBitString()).ToArray());
170+
result.ShouldBe("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111110101011010011101111111000000001000001001111100001110100000100000000101110101110000011000010111010000000010111010111010111100101011101000000001011101010011010111010101110100000000100000100011010001110010000010000000011111110101010101010101111111000000000000000000101000011100000000000000000111100101011110101011100111010000000001011000101011111010011101010000000001010011101111101001111011101000000000111011011110000010001100000100000000000000010011010101100000000000000000001100110101011011111001101110000000000000011100001010101010110101000000000000111001011100110111111110011000000001110101011001011001000100011000000000000101010100001010111111000000000000010111010101001111100000001110000000000010110100010111111100100010100000000011101111010011101111111101010000000000000000110000001000100010010000000001111111001100011001010101101000000000100000100111111111011000111000000000010111010010100011010111110111000000001011101010110100011100101011000000000101110101100101111100101111010000000010000010111011001111000001101000000001111111011110000100000110101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
171+
}
172+
173+
[Fact]
174+
[Category("QRGenerator/TextEncoding")]
175+
public void can_encode_utf8_bom()
176+
{
177+
var gen = new QRCodeGenerator();
178+
var qrData = gen.CreateQrCode("https://en.wikipedia.org/wiki/🍕", QRCodeGenerator.ECCLevel.L, true, true, QRCodeGenerator.EciMode.Utf8);
179+
var result = string.Join("", qrData.ModuleMatrix.Select(x => x.ToBitString()).ToArray());
180+
result.ShouldBe("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011111110010001101010101111111000000001000001011011000110000100000100000000101110100111010101111010111010000000010111010110100100010101011101000000001011101000101111000010101110100000000100000101010000111000010000010000000011111110101010101010101111111000000000000000000001010101110000000000000000111110111110101010100101010100000000000100000110000101000001100101000000000001001001011000011010000111100000000100010001111000001111110111010000000010110111010100011100100101111000000000001010001101101001000010100100000000100001101110011001010000001010000000001011001100011001111111010111000000000010001010101011110010100000100000000100100010000000000010110010000000000010110110010110000101010101100000000001001100100010010100111101101100000000101010110011000111101111100100000000000000000111011110011100011010000000001111111011100110010010101110000000000100000100100110010101000110110000000010111010110010111101111110011000000001011101010100000100010110100000000000101110101001100111110110111100000000010000010111100101111100100001000000001111111011110001110100111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
181+
}
182+
163183
[Fact]
164184
[Category("QRGenerator/TextEncoding")]
165185
public void can_generate_from_bytes()
@@ -170,6 +190,32 @@ public void can_generate_from_bytes()
170190
var result = string.Join("", qrData.ModuleMatrix.Select(x => x.ToBitString()).ToArray());
171191
result.ShouldBe("0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001111111011001011111110000000010000010010010100000100000000101110101010101011101000000001011101010010010111010000000010111010111000101110100000000100000100000001000001000000001111111010101011111110000000000000000011000000000000000000111100101010010011101000000001011100001001001001110000000010101011111011111110100000000000101000000110000000000000001011001001010100110000000000000000000110001000101000000000111111100110011011110000000001000001001111110111010000000010111010011100100101100000000101110101110010010010000000001011101011010100011000000000010000010110110101000100000000111111101011100010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
172192
}
193+
194+
[Fact]
195+
[Category("QRGenerator/TextEncoding")]
196+
public void isValidIso_works()
197+
{
198+
// see private method: QRCodeGenerator.IsValidISO
199+
200+
Encoding _iso88591ExceptionFallback = Encoding.GetEncoding(28591, new EncoderExceptionFallback(), new DecoderExceptionFallback()); // ISO-8859-1
201+
202+
IsValidISO("abc").ShouldBeTrue();
203+
IsValidISO("äöü").ShouldBeTrue();
204+
IsValidISO("🍕").ShouldBeFalse();
205+
206+
bool IsValidISO(string input)
207+
{
208+
try
209+
{
210+
_ = _iso88591ExceptionFallback.GetByteCount(input);
211+
return true;
212+
}
213+
catch (EncoderFallbackException)
214+
{
215+
return false;
216+
}
217+
}
218+
}
173219
}
174220

175221
public static class ExtensionMethods

0 commit comments

Comments
 (0)