diff --git a/QRCoder/QRCodeGenerator.cs b/QRCoder/QRCodeGenerator.cs index eec9c884..09b681aa 100644 --- a/QRCoder/QRCodeGenerator.cs +++ b/QRCoder/QRCodeGenerator.cs @@ -1,8 +1,10 @@ #if HAS_SPAN using System.Buffers; #endif +using System; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Text; namespace QRCoder; @@ -109,14 +111,39 @@ public static QRCodeData GenerateQrCode(PayloadGenerator.Payload payload, ECCLev public static QRCodeData GenerateQrCode(string plainText, ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, EciMode eciMode = EciMode.Default, int requestedVersion = -1) { eccLevel = ValidateECCLevel(eccLevel); + // Create data segment from plain text + var segment = CreateDataSegment(plainText, forceUtf8, utf8BOM, eciMode); + // Determine the appropriate version based on segment bit length + int version = DetermineVersion(segment, eccLevel, requestedVersion); + // Build the complete bit array for the determined version + var completeBitArray = BuildBitArrayFromSegment(segment, version); + return GenerateQrCode(completeBitArray, eccLevel, version); + } + + /// + /// Creates a data segment from plain text, encoding it appropriately. + /// + 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); - int version = requestedVersion; - int minVersion = CapacityTables.CalculateMinimumVersion(dataInputLength + (eciMode != EciMode.Default ? 2 : 0), encoding, eccLevel); - if (version == -1) + return new DataSegment(encoding, dataInputLength, codedText, eciMode); + } + + /// + /// Determines the appropriate QR code version based on the data segment and error correction level. + /// Validates that the data fits within the requested version, or finds the minimum version if not specified. + /// + private static int DetermineVersion(DataSegment segment, ECCLevel eccLevel, int version) + { + if (!CapacityTables.TryCalculateMinimumVersion(segment, eccLevel, out var minVersion)) { - version = minVersion; + return Throw(eccLevel, segment.EncodingMode, version == -1 ? 40 : version); + } + else if (version == -1) + { + return minVersion; } else { @@ -124,19 +151,30 @@ public static QRCodeData GenerateQrCode(string plainText, ECCLevel eccLevel, boo if (minVersion > version) { // Use a throw-helper to avoid allocating a closure - Throw(eccLevel, encoding, version); - - static void Throw(ECCLevel eccLevel, EncodingMode encoding, int version) - { - var maxSizeByte = CapacityTables.GetVersionInfo(version).Details.First(x => x.ErrorCorrectionLevel == eccLevel).CapacityDict[encoding]; - throw new Exceptions.DataTooLongException(eccLevel.ToString(), encoding.ToString(), version, maxSizeByte); - } + return Throw(eccLevel, segment.EncodingMode, version); } + return version; } - var modeIndicatorLength = eciMode != EciMode.Default ? 16 : 4; - var countIndicatorLength = GetCountIndicatorLength(version, encoding); - var completeBitArrayLength = modeIndicatorLength + countIndicatorLength + codedText.Length; + static int Throw(ECCLevel eccLevel, EncodingMode encoding, int version) + { + var maxSizeByte = CapacityTables.GetVersionInfo(version).Details.First(x => x.ErrorCorrectionLevel == eccLevel).CapacityDict[encoding]; + throw new Exceptions.DataTooLongException(eccLevel.ToString(), encoding.ToString(), version, maxSizeByte); + } + } + + /// + /// Builds a complete BitArray from a data segment for a specific QR code version. + /// + private static BitArray BuildBitArrayFromSegment(DataSegment segment, int version) + { + // todo in subsequent PR: eliminate these local variables and directly access the struct + var eciMode = segment.EciMode; + var encoding = segment.EncodingMode; + int dataInputLength = segment.CharacterCount; + var codedText = segment.Data; + int completeBitArrayLength = segment.GetBitLength(version); + int countIndicatorLength = GetCountIndicatorLength(version, segment.EncodingMode); var completeBitArray = new BitArray(completeBitArrayLength); @@ -156,7 +194,7 @@ static void Throw(ECCLevel eccLevel, EncodingMode encoding, int version) completeBitArray[completeBitArrayIndex++] = codedText[i]; } - return GenerateQrCode(completeBitArray, eccLevel, version); + return completeBitArray; } /// @@ -366,6 +404,7 @@ List CalculateECCBlocks() void AddCodeWordBlocks(int blockNum, int blocksInGroup, int codewordsInGroup, int offset2, int count, Polynom generatorPolynom) { + _ = blockNum; var groupLength = codewordsInGroup * 8; groupLength = groupLength > count ? count : groupLength; for (var i = 0; i < blocksInGroup; i++) diff --git a/QRCoder/QRCodeGenerator/CapacityTables.cs b/QRCoder/QRCodeGenerator/CapacityTables.cs index f4c6b94c..9146976e 100644 --- a/QRCoder/QRCodeGenerator/CapacityTables.cs +++ b/QRCoder/QRCodeGenerator/CapacityTables.cs @@ -116,6 +116,55 @@ static void Throw(EncodingMode encMode, ECCLevel eccLevel) } } + /// + /// Attempts to determine the minimum QR code version required to encode a data segment with a specific error correction level. + /// This method accounts for the version-dependent size of mode and count indicators when calculating the total bit length. + /// + /// The data segment to be encoded (includes encoding mode, character count, and data bits). + /// The error correction level (e.g., Low, Medium, Quartile, High). + /// When this method returns, contains the minimum version number (1-40) that can accommodate the data segment if a suitable version was found; otherwise, 0. + /// if a suitable QR code version was found; otherwise, . + public static bool TryCalculateMinimumVersion(DataSegment segment, ECCLevel eccLevel, out int version) + { + // Versions 1-9: Count indicator length is constant within this range + var segmentBitLength = segment.GetBitLength(1); + for (version = 1; version <= 9; version++) + { + var eccInfo = GetEccInfo(version, eccLevel); + // Check if this version has enough capacity for the segment's total bits + if (eccInfo.TotalDataBits >= segmentBitLength) + { + return true; + } + } + + // Versions 10-26: Count indicator length is constant within this range + segmentBitLength = segment.GetBitLength(10); + for (version = 10; version <= 26; version++) + { + var eccInfo = GetEccInfo(version, eccLevel); + // Check if this version has enough capacity for the segment's total bits + if (eccInfo.TotalDataBits >= segmentBitLength) + { + return true; + } + } + + // Versions 27-40: Count indicator length is constant within this range + segmentBitLength = segment.GetBitLength(27); + for (version = 27; version <= 40; version++) + { + var eccInfo = GetEccInfo(version, eccLevel); + // Check if this version has enough capacity for the segment's total bits + if (eccInfo.TotalDataBits >= segmentBitLength) + { + return true; + } + } + + version = 0; + return false; + } /// /// Determines the minimum Micro QR code version required to encode a given amount of data with a specific encoding mode and error correction level. diff --git a/QRCoder/QRCodeGenerator/DataSegment.cs b/QRCoder/QRCodeGenerator/DataSegment.cs new file mode 100644 index 00000000..0769d26b --- /dev/null +++ b/QRCoder/QRCodeGenerator/DataSegment.cs @@ -0,0 +1,55 @@ +namespace QRCoder; + +public partial class QRCodeGenerator +{ + /// + /// Represents a data segment for QR code encoding, containing the encoding mode, character count, and encoded data. + /// + private readonly struct DataSegment + { + /// + /// The encoding mode for this segment (Numeric, Alphanumeric, Byte, etc.) + /// + public readonly EncodingMode EncodingMode; + + /// + /// The character count (or byte count for byte mode) + /// + public readonly int CharacterCount; + + /// + /// The encoded data as a BitArray + /// + public readonly BitArray Data; + + /// + /// 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 readonly EciMode EciMode; + + public DataSegment(EncodingMode encodingMode, int characterCount, BitArray data, EciMode eciMode) + { + EncodingMode = encodingMode; + CharacterCount = characterCount; + Data = data; + 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 including mode indicator, count indicator, and data + public int GetBitLength(int version) + { + int modeIndicatorLength = HasEciMode ? 16 : 4; + int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode); + return modeIndicatorLength + countIndicatorLength + Data.Length; + } + } +}