Skip to content

Commit ab04613

Browse files
authored
Refactor conversion of text to data segments (#670)
* Refactor conversion of text to data segments * update * Refactor TryCalculateMinimumVersion method to improve version handling for QR code capacity
1 parent 6c3fd5e commit ab04613

File tree

3 files changed

+158
-15
lines changed

3 files changed

+158
-15
lines changed

QRCoder/QRCodeGenerator.cs

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#if HAS_SPAN
22
using System.Buffers;
33
#endif
4+
using System;
45
using System.Diagnostics;
56
using System.Runtime.CompilerServices;
7+
using System.Text;
68

79
namespace QRCoder;
810

@@ -109,34 +111,70 @@ public static QRCodeData GenerateQrCode(PayloadGenerator.Payload payload, ECCLev
109111
public static QRCodeData GenerateQrCode(string plainText, ECCLevel eccLevel, bool forceUtf8 = false, bool utf8BOM = false, EciMode eciMode = EciMode.Default, int requestedVersion = -1)
110112
{
111113
eccLevel = ValidateECCLevel(eccLevel);
114+
// Create data segment from plain text
115+
var segment = CreateDataSegment(plainText, forceUtf8, utf8BOM, eciMode);
116+
// Determine the appropriate version based on segment bit length
117+
int version = DetermineVersion(segment, eccLevel, requestedVersion);
118+
// Build the complete bit array for the determined version
119+
var completeBitArray = BuildBitArrayFromSegment(segment, version);
120+
return GenerateQrCode(completeBitArray, eccLevel, version);
121+
}
122+
123+
/// <summary>
124+
/// Creates a data segment from plain text, encoding it appropriately.
125+
/// </summary>
126+
private static DataSegment CreateDataSegment(string plainText, bool forceUtf8, bool utf8BOM, EciMode eciMode)
127+
{
112128
var encoding = GetEncodingFromPlaintext(plainText, forceUtf8);
113129
var codedText = PlainTextToBinary(plainText, encoding, eciMode, utf8BOM, forceUtf8);
114130
var dataInputLength = GetDataLength(encoding, plainText, codedText, forceUtf8);
115-
int version = requestedVersion;
116-
int minVersion = CapacityTables.CalculateMinimumVersion(dataInputLength + (eciMode != EciMode.Default ? 2 : 0), encoding, eccLevel);
117-
if (version == -1)
131+
return new DataSegment(encoding, dataInputLength, codedText, eciMode);
132+
}
133+
134+
/// <summary>
135+
/// Determines the appropriate QR code version based on the data segment and error correction level.
136+
/// Validates that the data fits within the requested version, or finds the minimum version if not specified.
137+
/// </summary>
138+
private static int DetermineVersion(DataSegment segment, ECCLevel eccLevel, int version)
139+
{
140+
if (!CapacityTables.TryCalculateMinimumVersion(segment, eccLevel, out var minVersion))
118141
{
119-
version = minVersion;
142+
return Throw(eccLevel, segment.EncodingMode, version == -1 ? 40 : version);
143+
}
144+
else if (version == -1)
145+
{
146+
return minVersion;
120147
}
121148
else
122149
{
123150
//Version was passed as fixed version via parameter. Thus let's check if chosen version is valid.
124151
if (minVersion > version)
125152
{
126153
// Use a throw-helper to avoid allocating a closure
127-
Throw(eccLevel, encoding, version);
128-
129-
static void Throw(ECCLevel eccLevel, EncodingMode encoding, int version)
130-
{
131-
var maxSizeByte = CapacityTables.GetVersionInfo(version).Details.First(x => x.ErrorCorrectionLevel == eccLevel).CapacityDict[encoding];
132-
throw new Exceptions.DataTooLongException(eccLevel.ToString(), encoding.ToString(), version, maxSizeByte);
133-
}
154+
return Throw(eccLevel, segment.EncodingMode, version);
134155
}
156+
return version;
135157
}
136158

137-
var modeIndicatorLength = eciMode != EciMode.Default ? 16 : 4;
138-
var countIndicatorLength = GetCountIndicatorLength(version, encoding);
139-
var completeBitArrayLength = modeIndicatorLength + countIndicatorLength + codedText.Length;
159+
static int Throw(ECCLevel eccLevel, EncodingMode encoding, int version)
160+
{
161+
var maxSizeByte = CapacityTables.GetVersionInfo(version).Details.First(x => x.ErrorCorrectionLevel == eccLevel).CapacityDict[encoding];
162+
throw new Exceptions.DataTooLongException(eccLevel.ToString(), encoding.ToString(), version, maxSizeByte);
163+
}
164+
}
165+
166+
/// <summary>
167+
/// Builds a complete BitArray from a data segment for a specific QR code version.
168+
/// </summary>
169+
private static BitArray BuildBitArrayFromSegment(DataSegment segment, int version)
170+
{
171+
// todo in subsequent PR: eliminate these local variables and directly access the struct
172+
var eciMode = segment.EciMode;
173+
var encoding = segment.EncodingMode;
174+
int dataInputLength = segment.CharacterCount;
175+
var codedText = segment.Data;
176+
int completeBitArrayLength = segment.GetBitLength(version);
177+
int countIndicatorLength = GetCountIndicatorLength(version, segment.EncodingMode);
140178

141179
var completeBitArray = new BitArray(completeBitArrayLength);
142180

@@ -156,7 +194,7 @@ static void Throw(ECCLevel eccLevel, EncodingMode encoding, int version)
156194
completeBitArray[completeBitArrayIndex++] = codedText[i];
157195
}
158196

159-
return GenerateQrCode(completeBitArray, eccLevel, version);
197+
return completeBitArray;
160198
}
161199

162200
/// <summary>
@@ -366,6 +404,7 @@ List<CodewordBlock> CalculateECCBlocks()
366404

367405
void AddCodeWordBlocks(int blockNum, int blocksInGroup, int codewordsInGroup, int offset2, int count, Polynom generatorPolynom)
368406
{
407+
_ = blockNum;
369408
var groupLength = codewordsInGroup * 8;
370409
groupLength = groupLength > count ? count : groupLength;
371410
for (var i = 0; i < blocksInGroup; i++)

QRCoder/QRCodeGenerator/CapacityTables.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,55 @@ static void Throw(EncodingMode encMode, ECCLevel eccLevel)
116116
}
117117
}
118118

119+
/// <summary>
120+
/// Attempts to determine the minimum QR code version required to encode a data segment with a specific error correction level.
121+
/// This method accounts for the version-dependent size of mode and count indicators when calculating the total bit length.
122+
/// </summary>
123+
/// <param name="segment">The data segment to be encoded (includes encoding mode, character count, and data bits).</param>
124+
/// <param name="eccLevel">The error correction level (e.g., Low, Medium, Quartile, High).</param>
125+
/// <param name="version">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.</param>
126+
/// <returns><see langword="true"/> if a suitable QR code version was found; otherwise, <see langword="false"/>.</returns>
127+
public static bool TryCalculateMinimumVersion(DataSegment segment, ECCLevel eccLevel, out int version)
128+
{
129+
// Versions 1-9: Count indicator length is constant within this range
130+
var segmentBitLength = segment.GetBitLength(1);
131+
for (version = 1; version <= 9; version++)
132+
{
133+
var eccInfo = GetEccInfo(version, eccLevel);
134+
// Check if this version has enough capacity for the segment's total bits
135+
if (eccInfo.TotalDataBits >= segmentBitLength)
136+
{
137+
return true;
138+
}
139+
}
140+
141+
// Versions 10-26: Count indicator length is constant within this range
142+
segmentBitLength = segment.GetBitLength(10);
143+
for (version = 10; version <= 26; version++)
144+
{
145+
var eccInfo = GetEccInfo(version, eccLevel);
146+
// Check if this version has enough capacity for the segment's total bits
147+
if (eccInfo.TotalDataBits >= segmentBitLength)
148+
{
149+
return true;
150+
}
151+
}
152+
153+
// Versions 27-40: Count indicator length is constant within this range
154+
segmentBitLength = segment.GetBitLength(27);
155+
for (version = 27; version <= 40; version++)
156+
{
157+
var eccInfo = GetEccInfo(version, eccLevel);
158+
// Check if this version has enough capacity for the segment's total bits
159+
if (eccInfo.TotalDataBits >= segmentBitLength)
160+
{
161+
return true;
162+
}
163+
}
164+
165+
version = 0;
166+
return false;
167+
}
119168

120169
/// <summary>
121170
/// Determines the minimum Micro QR code version required to encode a given amount of data with a specific encoding mode and error correction level.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
namespace QRCoder;
2+
3+
public partial class QRCodeGenerator
4+
{
5+
/// <summary>
6+
/// Represents a data segment for QR code encoding, containing the encoding mode, character count, and encoded data.
7+
/// </summary>
8+
private readonly struct DataSegment
9+
{
10+
/// <summary>
11+
/// The encoding mode for this segment (Numeric, Alphanumeric, Byte, etc.)
12+
/// </summary>
13+
public readonly EncodingMode EncodingMode;
14+
15+
/// <summary>
16+
/// The character count (or byte count for byte mode)
17+
/// </summary>
18+
public readonly int CharacterCount;
19+
20+
/// <summary>
21+
/// The encoded data as a BitArray
22+
/// </summary>
23+
public readonly BitArray Data;
24+
25+
/// <summary>
26+
/// Whether this segment includes an ECI mode indicator
27+
/// </summary>
28+
public bool HasEciMode => EciMode != EciMode.Default;
29+
30+
/// <summary>
31+
/// The ECI mode value (only valid if HasEciMode is true)
32+
/// </summary>
33+
public readonly EciMode EciMode;
34+
35+
public DataSegment(EncodingMode encodingMode, int characterCount, BitArray data, EciMode eciMode)
36+
{
37+
EncodingMode = encodingMode;
38+
CharacterCount = characterCount;
39+
Data = data;
40+
EciMode = eciMode;
41+
}
42+
43+
/// <summary>
44+
/// Calculates the total bit length for this segment when encoded for a specific QR code version.
45+
/// </summary>
46+
/// <param name="version">The QR code version (1-40, or -1 to -4 for Micro QR)</param>
47+
/// <returns>The total number of bits required for this segment including mode indicator, count indicator, and data</returns>
48+
public int GetBitLength(int version)
49+
{
50+
int modeIndicatorLength = HasEciMode ? 16 : 4;
51+
int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode);
52+
return modeIndicatorLength + countIndicatorLength + Data.Length;
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)