From 41dbc0b088180945fff6d7c9838cfee3d503c6c3 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Mon, 13 Oct 2025 23:27:35 -0400 Subject: [PATCH 1/9] Refactor DataSegment class to support multiple encoding modes and improve structure --- QRCoder/QRCodeGenerator.cs | 2 +- QRCoder/QRCodeGenerator/DataSegment.cs | 384 +++++++++++++++++++++++-- 2 files changed, 361 insertions(+), 25 deletions(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index 5196e7ec..e9917b43 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -128,7 +128,7 @@ private static DataSegment CreateDataSegment(string plainText, bool forceUtf8, b var encoding = GetEncodingFromPlaintext(plainText, forceUtf8); var codedText = PlainTextToBinary(plainText, encoding, eciMode, utf8BOM, forceUtf8); var dataInputLength = GetDataLength(encoding, plainText, codedText, forceUtf8); - return new DataSegment(encoding, dataInputLength, codedText, eciMode); + return new StandardDataSegment(encoding, dataInputLength, codedText, eciMode); } /// diff --git a/QRCoder/QRCodeGenerator/DataSegment.cs b/QRCoder/QRCodeGenerator/DataSegment.cs index bf3b1a4f..09202555 100644 --- a/QRCoder/QRCodeGenerator/DataSegment.cs +++ b/QRCoder/QRCodeGenerator/DataSegment.cs @@ -3,14 +3,82 @@ namespace QRCoder; public partial class QRCodeGenerator { /// - /// Represents a data segment for QR code encoding, containing the encoding mode, character count, and encoded data. + /// Represents an abstract data segment for QR code encoding. /// - private class DataSegment + private abstract class DataSegment + { + /// + /// The text to encode + /// +#if HAS_SPAN + public ReadOnlyMemory Text { get; } +#else + public string Text { get; } +#endif + + /// + /// The next data segment in the chain, or null if this is the last segment + /// + public DataSegment? Next { get; set; } + + /// + /// Gets the encoding mode for this segment (Numeric, Alphanumeric, Byte, etc.) + /// + public abstract EncodingMode EncodingMode { get; } + + /// + /// Initializes a new instance of the DataSegment class. + /// + /// The text to encode +#if HAS_SPAN + protected DataSegment(ReadOnlyMemory text) +#else + protected DataSegment(string text) +#endif + { + Text = text; + } + + /// + /// Writes this data segment to an existing BitArray at the specified index for a specific QR code version. + /// Chains to the next segment if present. + /// + /// The target BitArray to write to + /// The starting index in the BitArray where writing should begin + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The next index in the BitArray after the last bit written + public abstract int WriteTo(BitArray bitArray, int startIndex, int version); + + /// + /// Builds a complete BitArray from this data segment for a specific QR code version. + /// + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// A BitArray containing the complete encoded segment + public BitArray ToBitArray(int version) + { + var bitArray = new BitArray(GetBitLength(version)); + WriteTo(bitArray, 0, version); + return bitArray; + } + + /// + /// Calculates the total bit length for this segment when encoded for a specific QR code version. + /// Includes the length of all chained segments. + /// + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The total number of bits required for this segment including mode indicator, count indicator, and data + public abstract int GetBitLength(int version); + } + + /// + /// Standard implementation of a data segment for QR code encoding, containing the encoding mode, character count, and encoded data. + /// + private class StandardDataSegment : DataSegment { /// /// The encoding mode for this segment (Numeric, Alphanumeric, Byte, etc.) /// - public EncodingMode EncodingMode { get; } + public override EncodingMode EncodingMode { get; } /// /// The character count (or byte count for byte mode) @@ -22,11 +90,6 @@ private class DataSegment /// public BitArray Data { get; } - /// - /// The next data segment in the chain, or null if this is the last segment - /// - public DataSegment? Next { get; set; } - /// /// Whether this segment includes an ECI mode indicator /// @@ -38,13 +101,20 @@ private class DataSegment public EciMode EciMode { get; } /// - /// Initializes a new instance of the DataSegment struct. + /// Initializes a new instance of the StandardDataSegment class. /// /// The encoding mode for this segment (Numeric, Alphanumeric, Byte, etc.) /// The character count (or byte count for byte mode) /// The encoded data as a BitArray /// The ECI mode value (use EciMode.Default if not using ECI) - public DataSegment(EncodingMode encodingMode, int characterCount, BitArray data, EciMode eciMode) + public StandardDataSegment(EncodingMode encodingMode, int characterCount, BitArray data, EciMode eciMode) + : base( +#if HAS_SPAN + ReadOnlyMemory.Empty +#else + string.Empty +#endif + ) { EncodingMode = encodingMode; CharacterCount = characterCount; @@ -58,7 +128,7 @@ public DataSegment(EncodingMode encodingMode, int characterCount, BitArray data, /// /// The QR code version (1-40, or -1 to -4 for Micro QR) /// The total number of bits required for this segment including mode indicator, count indicator, and data - public int GetBitLength(int version) + public override int GetBitLength(int version) { int modeIndicatorLength = HasEciMode ? 16 : 4; int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode); @@ -71,18 +141,6 @@ public int GetBitLength(int version) return length; } - /// - /// Builds a complete BitArray from this data segment for a specific QR code version. - /// - /// The QR code version (1-40, or -1 to -4 for Micro QR) - /// A BitArray containing the complete encoded segment including mode indicator, count indicator, and data - public BitArray ToBitArray(int version) - { - var bitArray = new BitArray(GetBitLength(version)); - WriteTo(bitArray, 0, version); - return bitArray; - } - /// /// Writes this data segment to an existing BitArray at the specified index for a specific QR code version. /// Chains to the next segment if present. @@ -91,7 +149,7 @@ public BitArray ToBitArray(int version) /// The starting index in the BitArray where writing should begin /// The QR code version (1-40, or -1 to -4 for Micro QR) /// The next index in the BitArray after the last bit written - public int WriteTo(BitArray bitArray, int startIndex, int version) + public override int WriteTo(BitArray bitArray, int startIndex, int version) { var index = startIndex; @@ -119,4 +177,282 @@ public int WriteTo(BitArray bitArray, int startIndex, int version) return index; } } + + /// + /// Data segment optimized for numeric data encoding. + /// + private class NumericDataSegment : DataSegment + { + /// + /// Gets the encoding mode (always Numeric) + /// + public override EncodingMode EncodingMode => EncodingMode.Numeric; + + /// + /// Initializes a new instance of the NumericDataSegment class. + /// + /// The numeric text to encode (should only contain digits) +#if HAS_SPAN + public NumericDataSegment(ReadOnlyMemory numericText) +#else + public NumericDataSegment(string numericText) +#endif + : base(numericText) + { + } + + /// + /// Calculates the total bit length for this segment when encoded for a specific QR code version. + /// + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The total number of bits required for this segment + public override int GetBitLength(int version) + { + int modeIndicatorLength = 4; + int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Numeric); + int dataLength = Text.Length / 3 * 10 + (Text.Length % 3 == 1 ? 4 : Text.Length % 3 == 2 ? 7 : 0); + int length = modeIndicatorLength + countIndicatorLength + dataLength; + + // Add length of next segment if present + if (Next != null) + length += Next.GetBitLength(version); + + return length; + } + + /// + /// Writes this data segment to an existing BitArray at the specified index. + /// + /// The target BitArray to write to + /// The starting index in the BitArray + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The next index in the BitArray after the last bit written + public override int WriteTo(BitArray bitArray, int startIndex, int version) + { + var index = startIndex; + + // write mode indicator + index = DecToBin((int)EncodingMode.Numeric, 4, bitArray, index); + + // write count indicator + int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Numeric); + index = DecToBin(Text.Length, countIndicatorLength, bitArray, index); + + // write data - encode numeric text +#if HAS_SPAN + var data = PlainTextToBinaryNumeric(Text.ToString()); +#else + var data = PlainTextToBinaryNumeric(Text); +#endif + data.CopyTo(bitArray, 0, index, data.Length); + index += data.Length; + + // write next segment if present + if (Next != null) + index = Next.WriteTo(bitArray, index, version); + + return index; + } + } + + /// + /// Data segment optimized for alphanumeric data encoding. + /// + private class AlphanumericDataSegment : DataSegment + { + /// + /// Gets the encoding mode (always Alphanumeric) + /// + public override EncodingMode EncodingMode => EncodingMode.Alphanumeric; + + /// + /// Initializes a new instance of the AlphanumericDataSegment class. + /// + /// The alphanumeric text to encode +#if HAS_SPAN + public AlphanumericDataSegment(ReadOnlyMemory alphanumericText) +#else + public AlphanumericDataSegment(string alphanumericText) +#endif + : base(alphanumericText) + { + } + + /// + /// Calculates the total bit length for this segment when encoded for a specific QR code version. + /// + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The total number of bits required for this segment + public override int GetBitLength(int version) + { + int modeIndicatorLength = 4; + int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Alphanumeric); +#if HAS_SPAN + var data = AlphanumericEncoder.GetBitArray(Text.ToString()); +#else + var data = AlphanumericEncoder.GetBitArray(Text); +#endif + int length = modeIndicatorLength + countIndicatorLength + data.Length; + + // Add length of next segment if present + if (Next != null) + length += Next.GetBitLength(version); + + return length; + } + + /// + /// Writes this data segment to an existing BitArray at the specified index. + /// + /// The target BitArray to write to + /// The starting index in the BitArray + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The next index in the BitArray after the last bit written + public override int WriteTo(BitArray bitArray, int startIndex, int version) + { + var index = startIndex; + + // write mode indicator + index = DecToBin((int)EncodingMode.Alphanumeric, 4, bitArray, index); + + // write count indicator + int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Alphanumeric); + index = DecToBin(Text.Length, countIndicatorLength, bitArray, index); + + // write data - encode alphanumeric text +#if HAS_SPAN + var data = AlphanumericEncoder.GetBitArray(Text.ToString()); +#else + var data = AlphanumericEncoder.GetBitArray(Text); +#endif + data.CopyTo(bitArray, 0, index, data.Length); + index += data.Length; + + // write next segment if present + if (Next != null) + index = Next.WriteTo(bitArray, index, version); + + return index; + } + } + + /// + /// Data segment for byte mode encoding (used for UTF-8 and other text encodings). + /// + private class ByteDataSegment : DataSegment + { + /// + /// Whether to force UTF-8 encoding + /// + public bool ForceUtf8 { get; } + + /// + /// Whether to include UTF-8 BOM + /// + public bool Utf8BOM { get; } + + /// + /// The ECI mode to use + /// + public EciMode EciMode { get; } + + /// + /// Whether this segment includes an ECI mode indicator + /// + public bool HasEciMode => EciMode != EciMode.Default; + + /// + /// Gets the encoding mode (always Byte) + /// + public override EncodingMode EncodingMode => EncodingMode.Byte; + + /// + /// Initializes a new instance of the ByteDataSegment class. + /// + /// The text to encode + /// Whether to force UTF-8 encoding + /// Whether to include UTF-8 BOM + /// The ECI mode to use +#if HAS_SPAN + public ByteDataSegment(ReadOnlyMemory text, bool forceUtf8, bool utf8BOM, EciMode eciMode) +#else + public ByteDataSegment(string text, bool forceUtf8, bool utf8BOM, EciMode eciMode) +#endif + : base(text) + { + ForceUtf8 = forceUtf8; + Utf8BOM = utf8BOM; + EciMode = eciMode; + } + + /// + /// Calculates the total bit length for this segment when encoded for a specific QR code version. + /// + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The total number of bits required for this segment + public override int GetBitLength(int version) + { + int modeIndicatorLength = HasEciMode ? 16 : 4; + int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Byte); +#if HAS_SPAN + var textStr = Text.ToString(); + var data = PlainTextToBinaryByte(textStr, EciMode, Utf8BOM, ForceUtf8); + int dataLength = GetDataLength(EncodingMode.Byte, textStr, data, ForceUtf8); +#else + var data = PlainTextToBinaryByte(Text, EciMode, Utf8BOM, ForceUtf8); + int dataLength = GetDataLength(EncodingMode.Byte, Text, data, ForceUtf8); +#endif + int length = modeIndicatorLength + countIndicatorLength + data.Length; + + // Add length of next segment if present + if (Next != null) + length += Next.GetBitLength(version); + + return length; + } + + /// + /// Writes this data segment to an existing BitArray at the specified index. + /// + /// The target BitArray to write to + /// The starting index in the BitArray + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The next index in the BitArray after the last bit written + public override int WriteTo(BitArray bitArray, int startIndex, int version) + { + var index = startIndex; + + // write eci mode if present + if (HasEciMode) + { + index = DecToBin((int)EncodingMode.ECI, 4, bitArray, index); + index = DecToBin((int)EciMode, 8, bitArray, index); + } + + // write mode indicator + index = DecToBin((int)EncodingMode.Byte, 4, bitArray, index); + + // write count indicator +#if HAS_SPAN + var textStr = Text.ToString(); + var data = PlainTextToBinaryByte(textStr, EciMode, Utf8BOM, ForceUtf8); + int characterCount = GetDataLength(EncodingMode.Byte, textStr, data, ForceUtf8); +#else + var data = PlainTextToBinaryByte(Text, EciMode, Utf8BOM, ForceUtf8); + int characterCount = GetDataLength(EncodingMode.Byte, Text, data, ForceUtf8); +#endif + int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Byte); + index = DecToBin(characterCount, countIndicatorLength, bitArray, index); + + // write data + data.CopyTo(bitArray, 0, index, data.Length); + index += data.Length; + + // write next segment if present + if (Next != null) + index = Next.WriteTo(bitArray, index, version); + + return index; + } + } } From 44afd0bdb9bb64c5a2830759f19f82a22e51954e Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Mon, 13 Oct 2025 23:30:44 -0400 Subject: [PATCH 2/9] Refactor CreateDataSegment method to utilize specialized segment classes based on encoding mode --- QRCoder/QRCodeGenerator.cs | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index e9917b43..b1ac3abf 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -126,9 +126,33 @@ public static QRCodeData GenerateQrCode(string plainText, ECCLevel eccLevel, boo private static DataSegment CreateDataSegment(string plainText, bool forceUtf8, bool utf8BOM, EciMode eciMode) { var encoding = GetEncodingFromPlaintext(plainText, forceUtf8); - var codedText = PlainTextToBinary(plainText, encoding, eciMode, utf8BOM, forceUtf8); - var dataInputLength = GetDataLength(encoding, plainText, codedText, forceUtf8); - return new StandardDataSegment(encoding, dataInputLength, codedText, eciMode); + + // Use specialized segment classes based on encoding mode + return encoding switch + { + EncodingMode.Numeric => new NumericDataSegment( +#if HAS_SPAN + plainText.AsMemory() +#else + plainText +#endif + ), + EncodingMode.Alphanumeric => new AlphanumericDataSegment( +#if HAS_SPAN + plainText.AsMemory() +#else + plainText +#endif + ), + EncodingMode.Byte => new ByteDataSegment( +#if HAS_SPAN + plainText.AsMemory(), +#else + plainText, +#endif + forceUtf8, utf8BOM, eciMode), + _ => throw new InvalidOperationException($"Unsupported encoding mode: {encoding}") + }; } /// From 00047e44410e78c0de8bd19174d94fd274f5765b Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Mon, 13 Oct 2025 23:53:15 -0400 Subject: [PATCH 3/9] Refactor CreateDataSegment method to remove conditional compilation and simplify segment initialization --- QRCoder/QRCodeGenerator.cs | 24 +--- QRCoder/QRCodeGenerator/DataSegment.cs | 182 ------------------------- 2 files changed, 3 insertions(+), 203 deletions(-) diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index b1ac3abf..14505db9 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -130,27 +130,9 @@ private static DataSegment CreateDataSegment(string plainText, bool forceUtf8, b // Use specialized segment classes based on encoding mode return encoding switch { - EncodingMode.Numeric => new NumericDataSegment( -#if HAS_SPAN - plainText.AsMemory() -#else - plainText -#endif - ), - EncodingMode.Alphanumeric => new AlphanumericDataSegment( -#if HAS_SPAN - plainText.AsMemory() -#else - plainText -#endif - ), - EncodingMode.Byte => new ByteDataSegment( -#if HAS_SPAN - plainText.AsMemory(), -#else - plainText, -#endif - forceUtf8, utf8BOM, eciMode), + EncodingMode.Numeric => new NumericDataSegment(plainText), + EncodingMode.Alphanumeric => new AlphanumericDataSegment(plainText), + EncodingMode.Byte => new ByteDataSegment(plainText, forceUtf8, utf8BOM, eciMode), _ => throw new InvalidOperationException($"Unsupported encoding mode: {encoding}") }; } diff --git a/QRCoder/QRCodeGenerator/DataSegment.cs b/QRCoder/QRCodeGenerator/DataSegment.cs index 09202555..ad06c91a 100644 --- a/QRCoder/QRCodeGenerator/DataSegment.cs +++ b/QRCoder/QRCodeGenerator/DataSegment.cs @@ -10,16 +10,7 @@ private abstract class DataSegment /// /// The text to encode /// -#if HAS_SPAN - public ReadOnlyMemory Text { get; } -#else public string Text { get; } -#endif - - /// - /// The next data segment in the chain, or null if this is the last segment - /// - public DataSegment? Next { get; set; } /// /// Gets the encoding mode for this segment (Numeric, Alphanumeric, Byte, etc.) @@ -30,11 +21,7 @@ private abstract class DataSegment /// Initializes a new instance of the DataSegment class. /// /// The text to encode -#if HAS_SPAN - protected DataSegment(ReadOnlyMemory text) -#else protected DataSegment(string text) -#endif { Text = text; } @@ -70,114 +57,6 @@ public BitArray ToBitArray(int version) public abstract int GetBitLength(int version); } - /// - /// Standard implementation of a data segment for QR code encoding, containing the encoding mode, character count, and encoded data. - /// - private class StandardDataSegment : DataSegment - { - /// - /// The encoding mode for this segment (Numeric, Alphanumeric, Byte, etc.) - /// - public override EncodingMode EncodingMode { get; } - - /// - /// The character count (or byte count for byte mode) - /// - public int CharacterCount { get; } - - /// - /// The encoded data as a BitArray - /// - public BitArray Data { get; } - - /// - /// Whether this segment includes an ECI mode indicator - /// - public bool HasEciMode => EciMode != EciMode.Default; - - /// - /// The ECI mode value (only valid if HasEciMode is true) - /// - public EciMode EciMode { get; } - - /// - /// Initializes a new instance of the StandardDataSegment class. - /// - /// The encoding mode for this segment (Numeric, Alphanumeric, Byte, etc.) - /// The character count (or byte count for byte mode) - /// The encoded data as a BitArray - /// The ECI mode value (use EciMode.Default if not using ECI) - public StandardDataSegment(EncodingMode encodingMode, int characterCount, BitArray data, EciMode eciMode) - : base( -#if HAS_SPAN - ReadOnlyMemory.Empty -#else - string.Empty -#endif - ) - { - EncodingMode = encodingMode; - CharacterCount = characterCount; - Data = data; - EciMode = eciMode; - } - - /// - /// Calculates the total bit length for this segment when encoded for a specific QR code version. - /// Includes the length of all chained segments. - /// - /// The QR code version (1-40, or -1 to -4 for Micro QR) - /// The total number of bits required for this segment including mode indicator, count indicator, and data - public override int GetBitLength(int version) - { - int modeIndicatorLength = HasEciMode ? 16 : 4; - int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode); - int length = modeIndicatorLength + countIndicatorLength + Data.Length; - - // Add length of next segment if present - if (Next != null) - length += Next.GetBitLength(version); - - return length; - } - - /// - /// Writes this data segment to an existing BitArray at the specified index for a specific QR code version. - /// Chains to the next segment if present. - /// - /// The target BitArray to write to - /// The starting index in the BitArray where writing should begin - /// The QR code version (1-40, or -1 to -4 for Micro QR) - /// The next index in the BitArray after the last bit written - public override int WriteTo(BitArray bitArray, int startIndex, int version) - { - var index = startIndex; - - // write eci mode if present - if (HasEciMode) - { - index = DecToBin((int)EncodingMode.ECI, 4, bitArray, index); - index = DecToBin((int)EciMode, 8, bitArray, index); - } - // write mode indicator - index = DecToBin((int)EncodingMode, 4, bitArray, index); - - // write count indicator - int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode); - index = DecToBin(CharacterCount, countIndicatorLength, bitArray, index); - - // write data - Data.CopyTo(bitArray, 0, index, Data.Length); - index += Data.Length; - - // write next segment if present - if (Next != null) - index = Next.WriteTo(bitArray, index, version); - - return index; - } - } - /// /// Data segment optimized for numeric data encoding. /// @@ -192,11 +71,7 @@ private class NumericDataSegment : DataSegment /// Initializes a new instance of the NumericDataSegment class. /// /// The numeric text to encode (should only contain digits) -#if HAS_SPAN - public NumericDataSegment(ReadOnlyMemory numericText) -#else public NumericDataSegment(string numericText) -#endif : base(numericText) { } @@ -213,10 +88,6 @@ public override int GetBitLength(int version) int dataLength = Text.Length / 3 * 10 + (Text.Length % 3 == 1 ? 4 : Text.Length % 3 == 2 ? 7 : 0); int length = modeIndicatorLength + countIndicatorLength + dataLength; - // Add length of next segment if present - if (Next != null) - length += Next.GetBitLength(version); - return length; } @@ -239,18 +110,10 @@ public override int WriteTo(BitArray bitArray, int startIndex, int version) index = DecToBin(Text.Length, countIndicatorLength, bitArray, index); // write data - encode numeric text -#if HAS_SPAN - var data = PlainTextToBinaryNumeric(Text.ToString()); -#else var data = PlainTextToBinaryNumeric(Text); -#endif data.CopyTo(bitArray, 0, index, data.Length); index += data.Length; - // write next segment if present - if (Next != null) - index = Next.WriteTo(bitArray, index, version); - return index; } } @@ -269,11 +132,7 @@ private class AlphanumericDataSegment : DataSegment /// Initializes a new instance of the AlphanumericDataSegment class. /// /// The alphanumeric text to encode -#if HAS_SPAN - public AlphanumericDataSegment(ReadOnlyMemory alphanumericText) -#else public AlphanumericDataSegment(string alphanumericText) -#endif : base(alphanumericText) { } @@ -287,17 +146,9 @@ public override int GetBitLength(int version) { int modeIndicatorLength = 4; int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Alphanumeric); -#if HAS_SPAN - var data = AlphanumericEncoder.GetBitArray(Text.ToString()); -#else var data = AlphanumericEncoder.GetBitArray(Text); -#endif int length = modeIndicatorLength + countIndicatorLength + data.Length; - // Add length of next segment if present - if (Next != null) - length += Next.GetBitLength(version); - return length; } @@ -320,18 +171,10 @@ public override int WriteTo(BitArray bitArray, int startIndex, int version) index = DecToBin(Text.Length, countIndicatorLength, bitArray, index); // write data - encode alphanumeric text -#if HAS_SPAN - var data = AlphanumericEncoder.GetBitArray(Text.ToString()); -#else var data = AlphanumericEncoder.GetBitArray(Text); -#endif data.CopyTo(bitArray, 0, index, data.Length); index += data.Length; - // write next segment if present - if (Next != null) - index = Next.WriteTo(bitArray, index, version); - return index; } } @@ -373,11 +216,7 @@ private class ByteDataSegment : DataSegment /// Whether to force UTF-8 encoding /// Whether to include UTF-8 BOM /// The ECI mode to use -#if HAS_SPAN - public ByteDataSegment(ReadOnlyMemory text, bool forceUtf8, bool utf8BOM, EciMode eciMode) -#else public ByteDataSegment(string text, bool forceUtf8, bool utf8BOM, EciMode eciMode) -#endif : base(text) { ForceUtf8 = forceUtf8; @@ -394,20 +233,9 @@ public override int GetBitLength(int version) { int modeIndicatorLength = HasEciMode ? 16 : 4; int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Byte); -#if HAS_SPAN - var textStr = Text.ToString(); - var data = PlainTextToBinaryByte(textStr, EciMode, Utf8BOM, ForceUtf8); - int dataLength = GetDataLength(EncodingMode.Byte, textStr, data, ForceUtf8); -#else var data = PlainTextToBinaryByte(Text, EciMode, Utf8BOM, ForceUtf8); - int dataLength = GetDataLength(EncodingMode.Byte, Text, data, ForceUtf8); -#endif int length = modeIndicatorLength + countIndicatorLength + data.Length; - // Add length of next segment if present - if (Next != null) - length += Next.GetBitLength(version); - return length; } @@ -433,14 +261,8 @@ public override int WriteTo(BitArray bitArray, int startIndex, int version) index = DecToBin((int)EncodingMode.Byte, 4, bitArray, index); // write count indicator -#if HAS_SPAN - var textStr = Text.ToString(); - var data = PlainTextToBinaryByte(textStr, EciMode, Utf8BOM, ForceUtf8); - int characterCount = GetDataLength(EncodingMode.Byte, textStr, data, ForceUtf8); -#else var data = PlainTextToBinaryByte(Text, EciMode, Utf8BOM, ForceUtf8); int characterCount = GetDataLength(EncodingMode.Byte, Text, data, ForceUtf8); -#endif int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Byte); index = DecToBin(characterCount, countIndicatorLength, bitArray, index); @@ -448,10 +270,6 @@ public override int WriteTo(BitArray bitArray, int startIndex, int version) data.CopyTo(bitArray, 0, index, data.Length); index += data.Length; - // write next segment if present - if (Next != null) - index = Next.WriteTo(bitArray, index, version); - return index; } } From 9740625d4105a80c2460e4db276b62b355bfd5de Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Tue, 14 Oct 2025 00:05:36 -0400 Subject: [PATCH 4/9] Add GetBitLength and WriteToBitArray methods to AlphanumericEncoder for improved bit calculation and writing --- .../QRCodeGenerator/AlphanumericEncoder.cs | 36 +++++++++++++++---- QRCoder/QRCodeGenerator/DataSegment.cs | 8 ++--- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/QRCoder/QRCodeGenerator/AlphanumericEncoder.cs b/QRCoder/QRCodeGenerator/AlphanumericEncoder.cs index 3d48e468..25bde530 100644 --- a/QRCoder/QRCodeGenerator/AlphanumericEncoder.cs +++ b/QRCoder/QRCodeGenerator/AlphanumericEncoder.cs @@ -40,6 +40,16 @@ private static Dictionary CreateAlphanumEncDict(char[] alphanumEncTab /// public static bool CanEncodeNonDigit(char c) => IsInRange(c, 'A', 'Z') || Array.IndexOf(_alphanumEncTable, c) >= 0; + /// + /// Calculates the bit length required to encode the given alphanumeric text. + /// + /// The alphanumeric text to be encoded. + /// The number of bits required to encode the text. + public static int GetBitLength(string plainText) + { + return (plainText.Length / 2) * 11 + (plainText.Length & 1) * 6; + } + /// /// Converts alphanumeric plain text into a binary format optimized for QR codes. /// Alphanumeric encoding packs characters into 11-bit groups for each pair of characters, @@ -49,9 +59,23 @@ private static Dictionary CreateAlphanumEncDict(char[] alphanumEncTab /// A BitArray representing the binary data of the encoded alphanumeric text. public static BitArray GetBitArray(string plainText) { - // Calculate the length of the BitArray needed based on the number of character pairs. - var codeText = new BitArray((plainText.Length / 2) * 11 + (plainText.Length & 1) * 6); - var codeIndex = 0; + var codeText = new BitArray(GetBitLength(plainText)); + WriteToBitArray(plainText, codeText, 0); + return codeText; + } + + /// + /// Writes alphanumeric plain text directly into an existing BitArray at the specified index. + /// Alphanumeric encoding packs characters into 11-bit groups for each pair of characters, + /// and 6 bits for a single remaining character if the total count is odd. + /// + /// The alphanumeric text to be encoded, which should only contain characters valid in QR alphanumeric mode. + /// The target BitArray to write to. + /// The starting index in the BitArray where writing should begin. + /// The next index in the BitArray after the last bit written. + public static int WriteToBitArray(string plainText, BitArray bitArray, int startIndex) + { + var codeIndex = startIndex; var index = 0; var count = plainText.Length; @@ -61,17 +85,17 @@ public static BitArray GetBitArray(string plainText) // Convert each pair of characters to a number by looking them up in the alphanumeric dictionary and calculating. var dec = _alphanumEncDict[plainText[index++]] * 45 + _alphanumEncDict[plainText[index++]]; // Convert the number to binary and store it in the BitArray. - codeIndex = DecToBin(dec, 11, codeText, codeIndex); + codeIndex = DecToBin(dec, 11, bitArray, codeIndex); count -= 2; } // Handle the last character if the length is odd. if (count > 0) { - DecToBin(_alphanumEncDict[plainText[index]], 6, codeText, codeIndex); + codeIndex = DecToBin(_alphanumEncDict[plainText[index]], 6, bitArray, codeIndex); } - return codeText; + return codeIndex; } } } diff --git a/QRCoder/QRCodeGenerator/DataSegment.cs b/QRCoder/QRCodeGenerator/DataSegment.cs index ad06c91a..e90ebd65 100644 --- a/QRCoder/QRCodeGenerator/DataSegment.cs +++ b/QRCoder/QRCodeGenerator/DataSegment.cs @@ -146,8 +146,8 @@ public override int GetBitLength(int version) { int modeIndicatorLength = 4; int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Alphanumeric); - var data = AlphanumericEncoder.GetBitArray(Text); - int length = modeIndicatorLength + countIndicatorLength + data.Length; + int dataLength = AlphanumericEncoder.GetBitLength(Text); + int length = modeIndicatorLength + countIndicatorLength + dataLength; return length; } @@ -171,9 +171,7 @@ public override int WriteTo(BitArray bitArray, int startIndex, int version) index = DecToBin(Text.Length, countIndicatorLength, bitArray, index); // write data - encode alphanumeric text - var data = AlphanumericEncoder.GetBitArray(Text); - data.CopyTo(bitArray, 0, index, data.Length); - index += data.Length; + index = AlphanumericEncoder.WriteToBitArray(Text, bitArray, index); return index; } From b3ea2f0638d7175c1497a9cc0971352e9c069fd0 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Tue, 14 Oct 2025 00:08:56 -0400 Subject: [PATCH 5/9] Refactor WriteToBitArray method to improve parameter naming for clarity --- QRCoder/QRCodeGenerator/AlphanumericEncoder.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/QRCoder/QRCodeGenerator/AlphanumericEncoder.cs b/QRCoder/QRCodeGenerator/AlphanumericEncoder.cs index 25bde530..11b3da70 100644 --- a/QRCoder/QRCodeGenerator/AlphanumericEncoder.cs +++ b/QRCoder/QRCodeGenerator/AlphanumericEncoder.cs @@ -70,12 +70,11 @@ public static BitArray GetBitArray(string plainText) /// and 6 bits for a single remaining character if the total count is odd. /// /// The alphanumeric text to be encoded, which should only contain characters valid in QR alphanumeric mode. - /// The target BitArray to write to. - /// The starting index in the BitArray where writing should begin. + /// The target BitArray to write to. + /// The starting index in the BitArray where writing should begin. /// The next index in the BitArray after the last bit written. - public static int WriteToBitArray(string plainText, BitArray bitArray, int startIndex) + public static int WriteToBitArray(string plainText, BitArray codeText, int codeIndex) { - var codeIndex = startIndex; var index = 0; var count = plainText.Length; @@ -85,14 +84,14 @@ public static int WriteToBitArray(string plainText, BitArray bitArray, int start // Convert each pair of characters to a number by looking them up in the alphanumeric dictionary and calculating. var dec = _alphanumEncDict[plainText[index++]] * 45 + _alphanumEncDict[plainText[index++]]; // Convert the number to binary and store it in the BitArray. - codeIndex = DecToBin(dec, 11, bitArray, codeIndex); + codeIndex = DecToBin(dec, 11, codeText, codeIndex); count -= 2; } // Handle the last character if the length is odd. if (count > 0) { - codeIndex = DecToBin(_alphanumEncDict[plainText[index]], 6, bitArray, codeIndex); + codeIndex = DecToBin(_alphanumEncDict[plainText[index]], 6, codeText, codeIndex); } return codeIndex; From 474d6f183d5d328327d2729fc39b519b6e615d45 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Tue, 14 Oct 2025 00:14:32 -0400 Subject: [PATCH 6/9] Refactor ByteDataSegment to calculate character count directly from data length --- QRCoder/QRCodeGenerator/DataSegment.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QRCoder/QRCodeGenerator/DataSegment.cs b/QRCoder/QRCodeGenerator/DataSegment.cs index e90ebd65..f66702b0 100644 --- a/QRCoder/QRCodeGenerator/DataSegment.cs +++ b/QRCoder/QRCodeGenerator/DataSegment.cs @@ -260,7 +260,7 @@ public override int WriteTo(BitArray bitArray, int startIndex, int version) // write count indicator var data = PlainTextToBinaryByte(Text, EciMode, Utf8BOM, ForceUtf8); - int characterCount = GetDataLength(EncodingMode.Byte, Text, data, ForceUtf8); + int characterCount = data.Length / 8; int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Byte); index = DecToBin(characterCount, countIndicatorLength, bitArray, index); From 686e493ed9bc8dda6ebbf78ed6bc7a855f2c1578 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Tue, 14 Oct 2025 00:38:52 -0400 Subject: [PATCH 7/9] Refactor QR code data segment classes to improve encoding logic and structure --- QRCoder/QRCodeGenerator.cs | 167 ++---------- .../AlphanumericDataSegment.cs | 63 +++++ QRCoder/QRCodeGenerator/ByteDataSegment.cs | 252 ++++++++++++++++++ QRCoder/QRCodeGenerator/DataSegment.cs | 215 --------------- QRCoder/QRCodeGenerator/NumericDataSegment.cs | 114 ++++++++ 5 files changed, 448 insertions(+), 363 deletions(-) create mode 100644 QRCoder/QRCodeGenerator/AlphanumericDataSegment.cs create mode 100644 QRCoder/QRCodeGenerator/ByteDataSegment.cs create mode 100644 QRCoder/QRCodeGenerator/NumericDataSegment.cs diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index 14505db9..e73b32b5 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -1007,170 +1007,42 @@ private static BitArray PlainTextToBinary(string plainText, EncodingMode encMode private static readonly BitArray _emptyBitArray = new BitArray(0); /// - /// Converts numeric plain text into a binary format specifically optimized for QR codes. - /// Numeric compression groups up to 3 digits into 10 bits, less for remaining digits if they do not complete a group of three. + /// Converts an array of bytes into a BitArray, considering the proper bit order within each byte. + /// Unlike the constructor of BitArray, this function preserves the MSB-to-LSB order within each byte. /// - /// The numeric text to be encoded, which should only contain digit characters. - /// A BitArray representing the binary data of the encoded numeric text. - private static BitArray PlainTextToBinaryNumeric(string plainText) - { - // Calculate the length of the BitArray needed to encode the text. - // Groups of three digits are encoded in 10 bits, remaining groups of two or one digits take 7 or 4 bits respectively. - var bitArray = new BitArray(plainText.Length / 3 * 10 + (plainText.Length % 3 == 1 ? 4 : plainText.Length % 3 == 2 ? 7 : 0)); - var index = 0; - - // Process each group of three digits. - for (int i = 0; i < plainText.Length - 2; i += 3) - { - // Parse the next three characters as a decimal integer. -#if HAS_SPAN - var dec = int.Parse(plainText.AsSpan(i, 3), NumberStyles.None, CultureInfo.InvariantCulture); -#else - var dec = int.Parse(plainText.Substring(i, 3), NumberStyles.None, CultureInfo.InvariantCulture); -#endif - // Convert the decimal to binary and store it in the BitArray. - index = DecToBin(dec, 10, bitArray, index); - } - - // Handle any remaining digits if the total number is not a multiple of three. - if (plainText.Length % 3 == 2) // Two remaining digits are encoded in 7 bits. - { -#if HAS_SPAN - var dec = int.Parse(plainText.AsSpan(plainText.Length / 3 * 3, 2), NumberStyles.None, CultureInfo.InvariantCulture); -#else - var dec = int.Parse(plainText.Substring(plainText.Length / 3 * 3, 2), NumberStyles.None, CultureInfo.InvariantCulture); -#endif - index = DecToBin(dec, 7, bitArray, index); - } - else if (plainText.Length % 3 == 1) // One remaining digit is encoded in 4 bits. - { + /// 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( #if HAS_SPAN - var dec = int.Parse(plainText.AsSpan(plainText.Length / 3 * 3, 1), NumberStyles.None, CultureInfo.InvariantCulture); -#else - var dec = int.Parse(plainText.Substring(plainText.Length / 3 * 3, 1), NumberStyles.None, CultureInfo.InvariantCulture); -#endif - index = DecToBin(dec, 4, bitArray, index); - } - - return bitArray; - } - - private static readonly Encoding _iso8859_1 = -#if NET5_0_OR_GREATER - Encoding.Latin1; + ReadOnlySpan byteArray, // byte[] has an implicit cast to ReadOnlySpan #else - Encoding.GetEncoding(28591); // ISO-8859-1 + byte[] byteArray, #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). - /// - /// The text to be encoded. - /// The ECI mode that specifies the character encoding to use. - /// Specifies whether to include a Byte Order Mark (BOM) for UTF-8 encoding. - /// Forces UTF-8 encoding regardless of the text content's compatibility with ISO-8859-1. - /// A BitArray representing the binary data of the encoded text. - /// - /// The returned text is always encoded as ISO-8859-1 unless either the text contains a non-ISO-8859-1 character or - /// UTF-8 encoding is forced. This does not meet the QR Code standard, which requires the use of ECI to specify the encoding - /// when not ISO-8859-1. - /// - private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, bool utf8BOM, bool forceUtf8) + int prefixZeros = 0) { - 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) - { - targetEncoding = _iso8859_1; - utf8BOM = false; - } - else - { - // Determine the encoding based on the specified ECI mode. - switch (eciMode) - { - case EciMode.Iso8859_1: - // Convert text to ISO-8859-1 and encode. - 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. - _iso8859_2 ??= Encoding.GetEncoding(28592); // ISO-8859-2 - // Convert text to ISO-8859-2 and encode. - targetEncoding = _iso8859_2; - utf8BOM = false; - break; - case EciMode.Default: - case EciMode.Utf8: - default: - // Handle UTF-8 encoding, optionally adding a BOM if specified. - targetEncoding = Encoding.UTF8; - break; - } - } - -#if HAS_SPAN - // We can use stackalloc for small arrays to prevent heap allocations - const int MAX_STACK_SIZE_IN_BYTES = 512; - - int count = targetEncoding.GetByteCount(plainText); - 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 - 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); - } - else - { - bitArray = ToBitArray(codeBytes); - } - -#if HAS_SPAN - if (bufferFromPool != null) - ArrayPool.Shared.Return(bufferFromPool); -#endif - + // Calculate the total number of bits in the resulting BitArray including the prefix zeros. + var bitArray = new BitArray((int)((uint)byteArray.Length * 8) + prefixZeros); + CopyToBitArray(byteArray, bitArray, prefixZeros); return bitArray; } /// - /// Converts an array of bytes into a BitArray, considering the proper bit order within each byte. + /// Converts an array of bytes into a BitArray at a specified offset, considering the proper bit order within each byte. /// Unlike the constructor of BitArray, this function preserves the MSB-to-LSB order within each byte. /// /// 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( + /// The target BitArray to write to. + /// The starting offset in the BitArray where bits will be written. + private static void CopyToBitArray( #if HAS_SPAN ReadOnlySpan byteArray, // byte[] has an implicit cast to ReadOnlySpan #else byte[] byteArray, #endif - int prefixZeros = 0) + BitArray bitArray, + int offset) { - // Calculate the total number of bits in the resulting BitArray including the prefix zeros. - var bitArray = new BitArray((int)((uint)byteArray.Length * 8) + prefixZeros); for (var i = 0; i < byteArray.Length; i++) { var byteVal = byteArray[i]; @@ -1178,10 +1050,9 @@ private static BitArray ToBitArray( { // Set each bit in the BitArray based on the corresponding bit in the byte array. // It shifts bits within the byte to align with the MSB-to-LSB order. - bitArray[(int)((uint)i * 8) + j + prefixZeros] = (byteVal & (1 << (7 - j))) != 0; + bitArray[(int)((uint)i * 8) + j + offset] = (byteVal & (1 << (7 - j))) != 0; } } - return bitArray; } /// diff --git a/QRCoder/QRCodeGenerator/AlphanumericDataSegment.cs b/QRCoder/QRCodeGenerator/AlphanumericDataSegment.cs new file mode 100644 index 00000000..2b5b66a4 --- /dev/null +++ b/QRCoder/QRCodeGenerator/AlphanumericDataSegment.cs @@ -0,0 +1,63 @@ +namespace QRCoder; + +public partial class QRCodeGenerator +{ + /// + /// Data segment optimized for alphanumeric data encoding. + /// + private class AlphanumericDataSegment : DataSegment + { + /// + /// Gets the encoding mode (always Alphanumeric) + /// + public override EncodingMode EncodingMode => EncodingMode.Alphanumeric; + + /// + /// Initializes a new instance of the AlphanumericDataSegment class. + /// + /// The alphanumeric text to encode + public AlphanumericDataSegment(string alphanumericText) + : base(alphanumericText) + { + } + + /// + /// Calculates the total bit length for this segment when encoded for a specific QR code version. + /// + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The total number of bits required for this segment + public override int GetBitLength(int version) + { + int modeIndicatorLength = 4; + int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Alphanumeric); + int dataLength = AlphanumericEncoder.GetBitLength(Text); + int length = modeIndicatorLength + countIndicatorLength + dataLength; + + return length; + } + + /// + /// Writes this data segment to an existing BitArray at the specified index. + /// + /// The target BitArray to write to + /// The starting index in the BitArray + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The next index in the BitArray after the last bit written + public override int WriteTo(BitArray bitArray, int startIndex, int version) + { + var index = startIndex; + + // write mode indicator + index = DecToBin((int)EncodingMode.Alphanumeric, 4, bitArray, index); + + // write count indicator + int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Alphanumeric); + index = DecToBin(Text.Length, countIndicatorLength, bitArray, index); + + // write data - encode alphanumeric text + index = AlphanumericEncoder.WriteToBitArray(Text, bitArray, index); + + return index; + } + } +} diff --git a/QRCoder/QRCodeGenerator/ByteDataSegment.cs b/QRCoder/QRCodeGenerator/ByteDataSegment.cs new file mode 100644 index 00000000..c4ed0a67 --- /dev/null +++ b/QRCoder/QRCodeGenerator/ByteDataSegment.cs @@ -0,0 +1,252 @@ +#if HAS_SPAN +using System.Buffers; +#endif + +namespace QRCoder; + +public partial class QRCodeGenerator +{ + /// + /// Data segment for byte mode encoding (used for UTF-8 and other text encodings). + /// + private class ByteDataSegment : DataSegment + { + /// + /// Whether to force UTF-8 encoding + /// + public bool ForceUtf8 { get; } + + /// + /// Whether to include UTF-8 BOM + /// + public bool Utf8BOM { get; } + + /// + /// The ECI mode to use + /// + public EciMode EciMode { get; } + + /// + /// Whether this segment includes an ECI mode indicator + /// + public bool HasEciMode => EciMode != EciMode.Default; + + /// + /// Gets the encoding mode (always Byte) + /// + public override EncodingMode EncodingMode => EncodingMode.Byte; + + /// + /// Initializes a new instance of the ByteDataSegment class. + /// + /// The text to encode + /// Whether to force UTF-8 encoding + /// Whether to include UTF-8 BOM + /// The ECI mode to use + public ByteDataSegment(string text, bool forceUtf8, bool utf8BOM, EciMode eciMode) + : base(text) + { + ForceUtf8 = forceUtf8; + Utf8BOM = utf8BOM; + EciMode = eciMode; + } + + /// + /// Calculates the total bit length for this segment when encoded for a specific QR code version. + /// + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The total number of bits required for this segment + public override int GetBitLength(int version) + { + int modeIndicatorLength = HasEciMode ? 16 : 4; + int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Byte); + int dataBitLength = GetPlainTextToBinaryByteBitLength(Text, EciMode, Utf8BOM, ForceUtf8); + int length = modeIndicatorLength + countIndicatorLength + dataBitLength; + + return length; + } + + /// + /// Writes this data segment to an existing BitArray at the specified index. + /// + /// The target BitArray to write to + /// The starting index in the BitArray + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The next index in the BitArray after the last bit written + public override int WriteTo(BitArray bitArray, int startIndex, int version) + { + var index = startIndex; + + // write eci mode if present + if (HasEciMode) + { + index = DecToBin((int)EncodingMode.ECI, 4, bitArray, index); + index = DecToBin((int)EciMode, 8, bitArray, index); + } + + // write mode indicator + index = DecToBin((int)EncodingMode.Byte, 4, bitArray, index); + + // write count indicator + int dataBitLength = GetPlainTextToBinaryByteBitLength(Text, EciMode, Utf8BOM, ForceUtf8); + int characterCount = dataBitLength / 8; + int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Byte); + index = DecToBin(characterCount, countIndicatorLength, bitArray, index); + + // write data directly to the bit array + index = PlainTextToBinaryByte(Text, EciMode, Utf8BOM, ForceUtf8, bitArray, index); + + return index; + } + } + + private static readonly Encoding _iso8859_1 = +#if NET5_0_OR_GREATER + Encoding.Latin1; +#else + Encoding.GetEncoding(28591); // ISO-8859-1 +#endif + private static Encoding? _iso8859_2; + + /// + /// Determines the target encoding for the given text and encoding parameters. + /// + /// The text to be encoded. + /// The ECI mode that specifies the character encoding to use. + /// Specifies whether to include a Byte Order Mark (BOM) for UTF-8 encoding. + /// Forces UTF-8 encoding regardless of the text content's compatibility with ISO-8859-1. + /// Output parameter indicating whether the UTF-8 BOM should be included. + /// The encoding to use for the text. + private static Encoding GetTargetEncoding(string plainText, EciMode eciMode, bool utf8BOM, bool forceUtf8, out bool includeUtf8BOM) + { + 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) + { + targetEncoding = _iso8859_1; + includeUtf8BOM = false; + } + else + { + // Determine the encoding based on the specified ECI mode. + switch (eciMode) + { + case EciMode.Iso8859_1: + // Convert text to ISO-8859-1 and encode. + targetEncoding = _iso8859_1; + includeUtf8BOM = 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. + _iso8859_2 ??= Encoding.GetEncoding(28592); // ISO-8859-2 + // Convert text to ISO-8859-2 and encode. + targetEncoding = _iso8859_2; + includeUtf8BOM = false; + break; + case EciMode.Default: + case EciMode.Utf8: + default: + // Handle UTF-8 encoding, optionally adding a BOM if specified. + targetEncoding = Encoding.UTF8; + includeUtf8BOM = utf8BOM; + break; + } + } + + return targetEncoding; + } + + /// + /// Calculates the bit length required to encode plain text using byte mode encoding. + /// + /// The text to be encoded. + /// The ECI mode that specifies the character encoding to use. + /// Specifies whether to include a Byte Order Mark (BOM) for UTF-8 encoding. + /// Forces UTF-8 encoding regardless of the text content's compatibility with ISO-8859-1. + /// The number of bits required to encode the text. + private static int GetPlainTextToBinaryByteBitLength(string plainText, EciMode eciMode, bool utf8BOM, bool forceUtf8) + { + var targetEncoding = GetTargetEncoding(plainText, eciMode, utf8BOM, forceUtf8, out var includeUtf8BOM); + int byteCount = targetEncoding.GetByteCount(plainText); + return (byteCount * 8) + (includeUtf8BOM ? 24 : 0); + } + + /// + /// Converts plain text into a binary format using byte mode encoding, which supports various character encodings through ECI (Extended Channel Interpretations). + /// + /// The text to be encoded. + /// The ECI mode that specifies the character encoding to use. + /// Specifies whether to include a Byte Order Mark (BOM) for UTF-8 encoding. + /// Forces UTF-8 encoding regardless of the text content's compatibility with ISO-8859-1. + /// A BitArray representing the binary data of the encoded text. + /// + /// The returned text is always encoded as ISO-8859-1 unless either the text contains a non-ISO-8859-1 character or + /// UTF-8 encoding is forced. This does not meet the QR Code standard, which requires the use of ECI to specify the encoding + /// when not ISO-8859-1. + /// + private static BitArray PlainTextToBinaryByte(string plainText, EciMode eciMode, bool utf8BOM, bool forceUtf8) + { + int bitLength = GetPlainTextToBinaryByteBitLength(plainText, eciMode, utf8BOM, forceUtf8); + var bitArray = new BitArray(bitLength); + PlainTextToBinaryByte(plainText, eciMode, utf8BOM, forceUtf8, bitArray, 0); + return bitArray; + } + + /// + /// Converts plain text into a binary format using byte mode encoding, writing directly to an existing BitArray at the specified offset. + /// + /// The text to be encoded. + /// The ECI mode that specifies the character encoding to use. + /// Specifies whether to include a Byte Order Mark (BOM) for UTF-8 encoding. + /// Forces UTF-8 encoding regardless of the text content's compatibility with ISO-8859-1. + /// The target BitArray to write to. Must be large enough to hold the encoded data. + /// The starting offset in the BitArray where bits will be written. + /// The next offset in the BitArray after the last bit written. + /// + /// The returned text is always encoded as ISO-8859-1 unless either the text contains a non-ISO-8859-1 character or + /// UTF-8 encoding is forced. This does not meet the QR Code standard, which requires the use of ECI to specify the encoding + /// when not ISO-8859-1. + /// + private static int PlainTextToBinaryByte(string plainText, EciMode eciMode, bool utf8BOM, bool forceUtf8, BitArray bitArray, int offset) + { + var targetEncoding = GetTargetEncoding(plainText, eciMode, utf8BOM, forceUtf8, out var includeUtf8BOM); + +#if HAS_SPAN + // We can use stackalloc for small arrays to prevent heap allocations + const int MAX_STACK_SIZE_IN_BYTES = 512; + + int count = targetEncoding.GetByteCount(plainText); + 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 + + // Write the data to the BitArray + if (includeUtf8BOM) + { + // write UTF8 preamble (EF BB BF) to the BitArray + DecToBin(0xEF, 8, bitArray, offset); + DecToBin(0xBB, 8, bitArray, offset + 8); + DecToBin(0xBF, 8, bitArray, offset + 16); + offset += 24; + } + CopyToBitArray(codeBytes, bitArray, offset); + offset += (int)((uint)codeBytes.Length * 8); + +#if HAS_SPAN + if (bufferFromPool != null) + ArrayPool.Shared.Return(bufferFromPool); +#endif + + return offset; + } +} diff --git a/QRCoder/QRCodeGenerator/DataSegment.cs b/QRCoder/QRCodeGenerator/DataSegment.cs index f66702b0..e7914c9b 100644 --- a/QRCoder/QRCodeGenerator/DataSegment.cs +++ b/QRCoder/QRCodeGenerator/DataSegment.cs @@ -56,219 +56,4 @@ public BitArray ToBitArray(int version) /// The total number of bits required for this segment including mode indicator, count indicator, and data public abstract int GetBitLength(int version); } - - /// - /// Data segment optimized for numeric data encoding. - /// - private class NumericDataSegment : DataSegment - { - /// - /// Gets the encoding mode (always Numeric) - /// - public override EncodingMode EncodingMode => EncodingMode.Numeric; - - /// - /// Initializes a new instance of the NumericDataSegment class. - /// - /// The numeric text to encode (should only contain digits) - public NumericDataSegment(string numericText) - : base(numericText) - { - } - - /// - /// Calculates the total bit length for this segment when encoded for a specific QR code version. - /// - /// The QR code version (1-40, or -1 to -4 for Micro QR) - /// The total number of bits required for this segment - public override int GetBitLength(int version) - { - int modeIndicatorLength = 4; - int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Numeric); - int dataLength = Text.Length / 3 * 10 + (Text.Length % 3 == 1 ? 4 : Text.Length % 3 == 2 ? 7 : 0); - int length = modeIndicatorLength + countIndicatorLength + dataLength; - - return length; - } - - /// - /// Writes this data segment to an existing BitArray at the specified index. - /// - /// The target BitArray to write to - /// The starting index in the BitArray - /// The QR code version (1-40, or -1 to -4 for Micro QR) - /// The next index in the BitArray after the last bit written - public override int WriteTo(BitArray bitArray, int startIndex, int version) - { - var index = startIndex; - - // write mode indicator - index = DecToBin((int)EncodingMode.Numeric, 4, bitArray, index); - - // write count indicator - int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Numeric); - index = DecToBin(Text.Length, countIndicatorLength, bitArray, index); - - // write data - encode numeric text - var data = PlainTextToBinaryNumeric(Text); - data.CopyTo(bitArray, 0, index, data.Length); - index += data.Length; - - return index; - } - } - - /// - /// Data segment optimized for alphanumeric data encoding. - /// - private class AlphanumericDataSegment : DataSegment - { - /// - /// Gets the encoding mode (always Alphanumeric) - /// - public override EncodingMode EncodingMode => EncodingMode.Alphanumeric; - - /// - /// Initializes a new instance of the AlphanumericDataSegment class. - /// - /// The alphanumeric text to encode - public AlphanumericDataSegment(string alphanumericText) - : base(alphanumericText) - { - } - - /// - /// Calculates the total bit length for this segment when encoded for a specific QR code version. - /// - /// The QR code version (1-40, or -1 to -4 for Micro QR) - /// The total number of bits required for this segment - public override int GetBitLength(int version) - { - int modeIndicatorLength = 4; - int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Alphanumeric); - int dataLength = AlphanumericEncoder.GetBitLength(Text); - int length = modeIndicatorLength + countIndicatorLength + dataLength; - - return length; - } - - /// - /// Writes this data segment to an existing BitArray at the specified index. - /// - /// The target BitArray to write to - /// The starting index in the BitArray - /// The QR code version (1-40, or -1 to -4 for Micro QR) - /// The next index in the BitArray after the last bit written - public override int WriteTo(BitArray bitArray, int startIndex, int version) - { - var index = startIndex; - - // write mode indicator - index = DecToBin((int)EncodingMode.Alphanumeric, 4, bitArray, index); - - // write count indicator - int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Alphanumeric); - index = DecToBin(Text.Length, countIndicatorLength, bitArray, index); - - // write data - encode alphanumeric text - index = AlphanumericEncoder.WriteToBitArray(Text, bitArray, index); - - return index; - } - } - - /// - /// Data segment for byte mode encoding (used for UTF-8 and other text encodings). - /// - private class ByteDataSegment : DataSegment - { - /// - /// Whether to force UTF-8 encoding - /// - public bool ForceUtf8 { get; } - - /// - /// Whether to include UTF-8 BOM - /// - public bool Utf8BOM { get; } - - /// - /// The ECI mode to use - /// - public EciMode EciMode { get; } - - /// - /// Whether this segment includes an ECI mode indicator - /// - public bool HasEciMode => EciMode != EciMode.Default; - - /// - /// Gets the encoding mode (always Byte) - /// - public override EncodingMode EncodingMode => EncodingMode.Byte; - - /// - /// Initializes a new instance of the ByteDataSegment class. - /// - /// The text to encode - /// Whether to force UTF-8 encoding - /// Whether to include UTF-8 BOM - /// The ECI mode to use - public ByteDataSegment(string text, bool forceUtf8, bool utf8BOM, EciMode eciMode) - : base(text) - { - ForceUtf8 = forceUtf8; - Utf8BOM = utf8BOM; - EciMode = eciMode; - } - - /// - /// Calculates the total bit length for this segment when encoded for a specific QR code version. - /// - /// The QR code version (1-40, or -1 to -4 for Micro QR) - /// The total number of bits required for this segment - public override int GetBitLength(int version) - { - int modeIndicatorLength = HasEciMode ? 16 : 4; - int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Byte); - var data = PlainTextToBinaryByte(Text, EciMode, Utf8BOM, ForceUtf8); - int length = modeIndicatorLength + countIndicatorLength + data.Length; - - return length; - } - - /// - /// Writes this data segment to an existing BitArray at the specified index. - /// - /// The target BitArray to write to - /// The starting index in the BitArray - /// The QR code version (1-40, or -1 to -4 for Micro QR) - /// The next index in the BitArray after the last bit written - public override int WriteTo(BitArray bitArray, int startIndex, int version) - { - var index = startIndex; - - // write eci mode if present - if (HasEciMode) - { - index = DecToBin((int)EncodingMode.ECI, 4, bitArray, index); - index = DecToBin((int)EciMode, 8, bitArray, index); - } - - // write mode indicator - index = DecToBin((int)EncodingMode.Byte, 4, bitArray, index); - - // write count indicator - var data = PlainTextToBinaryByte(Text, EciMode, Utf8BOM, ForceUtf8); - int characterCount = data.Length / 8; - int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Byte); - index = DecToBin(characterCount, countIndicatorLength, bitArray, index); - - // write data - data.CopyTo(bitArray, 0, index, data.Length); - index += data.Length; - - return index; - } - } } diff --git a/QRCoder/QRCodeGenerator/NumericDataSegment.cs b/QRCoder/QRCodeGenerator/NumericDataSegment.cs new file mode 100644 index 00000000..87b4ac14 --- /dev/null +++ b/QRCoder/QRCodeGenerator/NumericDataSegment.cs @@ -0,0 +1,114 @@ +namespace QRCoder; + +public partial class QRCodeGenerator +{ + /// + /// Data segment optimized for numeric data encoding. + /// + private class NumericDataSegment : DataSegment + { + /// + /// Gets the encoding mode (always Numeric) + /// + public override EncodingMode EncodingMode => EncodingMode.Numeric; + + /// + /// Initializes a new instance of the NumericDataSegment class. + /// + /// The numeric text to encode (should only contain digits) + public NumericDataSegment(string numericText) + : base(numericText) + { + } + + /// + /// Calculates the total bit length for this segment when encoded for a specific QR code version. + /// + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The total number of bits required for this segment + public override int GetBitLength(int version) + { + int modeIndicatorLength = 4; + int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Numeric); + int dataLength = Text.Length / 3 * 10 + (Text.Length % 3 == 1 ? 4 : Text.Length % 3 == 2 ? 7 : 0); + int length = modeIndicatorLength + countIndicatorLength + dataLength; + + return length; + } + + /// + /// Writes this data segment to an existing BitArray at the specified index. + /// + /// The target BitArray to write to + /// The starting index in the BitArray + /// The QR code version (1-40, or -1 to -4 for Micro QR) + /// The next index in the BitArray after the last bit written + public override int WriteTo(BitArray bitArray, int startIndex, int version) + { + var index = startIndex; + + // write mode indicator + index = DecToBin((int)EncodingMode.Numeric, 4, bitArray, index); + + // write count indicator + int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode.Numeric); + index = DecToBin(Text.Length, countIndicatorLength, bitArray, index); + + // write data - encode numeric text + var data = PlainTextToBinaryNumeric(Text); + data.CopyTo(bitArray, 0, index, data.Length); + index += data.Length; + + return index; + } + } + + /// + /// Converts numeric plain text into a binary format specifically optimized for QR codes. + /// Numeric compression groups up to 3 digits into 10 bits, less for remaining digits if they do not complete a group of three. + /// + /// The numeric text to be encoded, which should only contain digit characters. + /// A BitArray representing the binary data of the encoded numeric text. + private static BitArray PlainTextToBinaryNumeric(string plainText) + { + // Calculate the length of the BitArray needed to encode the text. + // Groups of three digits are encoded in 10 bits, remaining groups of two or one digits take 7 or 4 bits respectively. + var bitArray = new BitArray(plainText.Length / 3 * 10 + (plainText.Length % 3 == 1 ? 4 : plainText.Length % 3 == 2 ? 7 : 0)); + var index = 0; + + // Process each group of three digits. + for (int i = 0; i < plainText.Length - 2; i += 3) + { + // Parse the next three characters as a decimal integer. +#if HAS_SPAN + var dec = int.Parse(plainText.AsSpan(i, 3), NumberStyles.None, CultureInfo.InvariantCulture); +#else + var dec = int.Parse(plainText.Substring(i, 3), NumberStyles.None, CultureInfo.InvariantCulture); +#endif + // Convert the decimal to binary and store it in the BitArray. + index = DecToBin(dec, 10, bitArray, index); + } + + // Handle any remaining digits if the total number is not a multiple of three. + if (plainText.Length % 3 == 2) // Two remaining digits are encoded in 7 bits. + { +#if HAS_SPAN + var dec = int.Parse(plainText.AsSpan(plainText.Length / 3 * 3, 2), NumberStyles.None, CultureInfo.InvariantCulture); +#else + var dec = int.Parse(plainText.Substring(plainText.Length / 3 * 3, 2), NumberStyles.None, CultureInfo.InvariantCulture); +#endif + index = DecToBin(dec, 7, bitArray, index); + } + else if (plainText.Length % 3 == 1) // One remaining digit is encoded in 4 bits. + { +#if HAS_SPAN + var dec = int.Parse(plainText.AsSpan(plainText.Length / 3 * 3, 1), NumberStyles.None, CultureInfo.InvariantCulture); +#else + var dec = int.Parse(plainText.Substring(plainText.Length / 3 * 3, 1), NumberStyles.None, CultureInfo.InvariantCulture); +#endif + index = DecToBin(dec, 4, bitArray, index); + } + + return bitArray; + } +} From f0329d7fd282a9445086d094ee55d139c2c80341 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Tue, 14 Oct 2025 11:22:32 -0400 Subject: [PATCH 8/9] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- QRCoder/QRCodeGenerator/AlphanumericDataSegment.cs | 2 +- QRCoder/QRCodeGenerator/ByteDataSegment.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/QRCoder/QRCodeGenerator/AlphanumericDataSegment.cs b/QRCoder/QRCodeGenerator/AlphanumericDataSegment.cs index 2b5b66a4..98fcd1c9 100644 --- a/QRCoder/QRCodeGenerator/AlphanumericDataSegment.cs +++ b/QRCoder/QRCodeGenerator/AlphanumericDataSegment.cs @@ -5,7 +5,7 @@ public partial class QRCodeGenerator /// /// Data segment optimized for alphanumeric data encoding. /// - private class AlphanumericDataSegment : DataSegment + private sealed class AlphanumericDataSegment : DataSegment { /// /// Gets the encoding mode (always Alphanumeric) diff --git a/QRCoder/QRCodeGenerator/ByteDataSegment.cs b/QRCoder/QRCodeGenerator/ByteDataSegment.cs index c4ed0a67..c95b48f9 100644 --- a/QRCoder/QRCodeGenerator/ByteDataSegment.cs +++ b/QRCoder/QRCodeGenerator/ByteDataSegment.cs @@ -9,7 +9,7 @@ public partial class QRCodeGenerator /// /// Data segment for byte mode encoding (used for UTF-8 and other text encodings). /// - private class ByteDataSegment : DataSegment + private sealed class ByteDataSegment : DataSegment { /// /// Whether to force UTF-8 encoding From 82d32e6fddcdaed54372d32f03aa446d1270e573 Mon Sep 17 00:00:00 2001 From: Shane Krueger Date: Tue, 14 Oct 2025 11:43:40 -0400 Subject: [PATCH 9/9] Update QRCoder/QRCodeGenerator/NumericDataSegment.cs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günther Foidl --- QRCoder/QRCodeGenerator/NumericDataSegment.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QRCoder/QRCodeGenerator/NumericDataSegment.cs b/QRCoder/QRCodeGenerator/NumericDataSegment.cs index 87b4ac14..61935736 100644 --- a/QRCoder/QRCodeGenerator/NumericDataSegment.cs +++ b/QRCoder/QRCodeGenerator/NumericDataSegment.cs @@ -5,7 +5,7 @@ public partial class QRCodeGenerator /// /// Data segment optimized for numeric data encoding. /// - private class NumericDataSegment : DataSegment + private sealed class NumericDataSegment : DataSegment { /// /// Gets the encoding mode (always Numeric)