Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 54 additions & 15 deletions QRCoder/QRCodeGenerator.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -109,34 +111,70 @@ 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);
}

/// <summary>
/// Creates a data segment from plain text, encoding it appropriately.
/// </summary>
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);
}

/// <summary>
/// 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.
/// </summary>
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
{
//Version was passed as fixed version via parameter. Thus let's check if chosen version is valid.
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);
}
}

/// <summary>
/// Builds a complete BitArray from a data segment for a specific QR code version.
/// </summary>
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);

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

return GenerateQrCode(completeBitArray, eccLevel, version);
return completeBitArray;
}

/// <summary>
Expand Down Expand Up @@ -366,6 +404,7 @@ List<CodewordBlock> 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++)
Expand Down
49 changes: 49 additions & 0 deletions QRCoder/QRCodeGenerator/CapacityTables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,55 @@ static void Throw(EncodingMode encMode, ECCLevel eccLevel)
}
}

/// <summary>
/// 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.
/// </summary>
/// <param name="segment">The data segment to be encoded (includes encoding mode, character count, and data bits).</param>
/// <param name="eccLevel">The error correction level (e.g., Low, Medium, Quartile, High).</param>
/// <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>
/// <returns><see langword="true"/> if a suitable QR code version was found; otherwise, <see langword="false"/>.</returns>
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;
}

/// <summary>
/// Determines the minimum Micro QR code version required to encode a given amount of data with a specific encoding mode and error correction level.
Expand Down
55 changes: 55 additions & 0 deletions QRCoder/QRCodeGenerator/DataSegment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
namespace QRCoder;

public partial class QRCodeGenerator
{
/// <summary>
/// Represents a data segment for QR code encoding, containing the encoding mode, character count, and encoded data.
/// </summary>
private readonly struct DataSegment
{
/// <summary>
/// The encoding mode for this segment (Numeric, Alphanumeric, Byte, etc.)
/// </summary>
public readonly EncodingMode EncodingMode;

/// <summary>
/// The character count (or byte count for byte mode)
/// </summary>
public readonly int CharacterCount;

/// <summary>
/// The encoded data as a BitArray
/// </summary>
public readonly BitArray Data;

/// <summary>
/// Whether this segment includes an ECI mode indicator
/// </summary>
public bool HasEciMode => EciMode != EciMode.Default;

/// <summary>
/// The ECI mode value (only valid if HasEciMode is true)
/// </summary>
public readonly EciMode EciMode;

public DataSegment(EncodingMode encodingMode, int characterCount, BitArray data, EciMode eciMode)
{
EncodingMode = encodingMode;
CharacterCount = characterCount;
Data = data;
EciMode = eciMode;
}

/// <summary>
/// Calculates the total bit length for this segment when encoded for a specific QR code version.
/// </summary>
/// <param name="version">The QR code version (1-40, or -1 to -4 for Micro QR)</param>
/// <returns>The total number of bits required for this segment including mode indicator, count indicator, and data</returns>
public int GetBitLength(int version)
{
int modeIndicatorLength = HasEciMode ? 16 : 4;
int countIndicatorLength = GetCountIndicatorLength(version, EncodingMode);
return modeIndicatorLength + countIndicatorLength + Data.Length;
}
}
}