Skip to content

Commit 05ed503

Browse files
committed
Support for structured append (mulitple linked QR codes)
1 parent 7074460 commit 05ed503

File tree

3 files changed

+332
-11
lines changed

3 files changed

+332
-11
lines changed

QrCodeGenerator/QrCode.cs

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
using System.Collections;
3030
using System.Collections.Generic;
3131
using System.Diagnostics;
32+
using System.Linq;
33+
using System.Text;
3234

3335
namespace Net.Codecrete.QrCodeGenerator
3436
{
@@ -91,6 +93,65 @@ public static QrCode EncodeText(string text, Ecc ecl)
9193
return EncodeSegments(segments, ecl);
9294
}
9395

96+
/// <summary>
97+
/// Creates a series of QR codes representing the specified text.
98+
/// <para>
99+
/// The result will consist of the minimal number of QR codes needed
100+
/// to encode the text with the given error correction level and version (size of QR code).
101+
/// If multiple QR codes are required, <em>Structured Append</em> data is included to link the QR codes.
102+
/// </para>
103+
/// <para>
104+
/// The text is split at character boundaries even though the underlying UTF-8 encoding requires
105+
/// multiple bytes for some characters. This increases compatibility with QR code scanners assuming
106+
/// that each individual QR codes contains a valid UTF-8 string.
107+
/// </para>
108+
/// </summary>
109+
/// <param name="text">The text to be encoded. The full range of Unicode characters may be used.</param>
110+
/// <param name="ecl">The minimum error correction level to use.</param>
111+
/// <param name="version">The version (size of QR code) to use. Default is 29.</param>
112+
/// <param name="boostEcl">Whether error correction level should be upgraded if possible. Default is false.</param>
113+
/// <returns>A list of QR codes representing the specified text.</returns>
114+
/// <exception cref="ArgumentNullException"><paramref name="text"/> or <paramref name="ecl"/> is <c>null</c>.</exception>
115+
/// <exception cref="DataTooLongException">The text is too long to fit the maximum of 16 QR codes with the given parameters.</exception>
116+
public static List<QrCode> EncodeTextInMultipleCodes(string text, Ecc ecl, int version = 29, bool boostEcl = true)
117+
{
118+
Objects.RequireNonNull(text);
119+
Objects.RequireNonNull(ecl);
120+
121+
var textBytes = Encoding.UTF8.GetBytes(text);
122+
var numCharCountBits = QrSegment.Mode.Byte.NumCharCountBits(version);
123+
var numDataCodewords = GetNumDataCodewords(version, ecl);
124+
125+
if ((numCharCountBits + 7) / 8 + textBytes.Length <= numDataCodewords)
126+
{
127+
// text is short enough to fit into a single QR code
128+
var segment = QrSegment.MakeBytes(textBytes);
129+
var qrCode = EncodeSegments(new List<QrSegment> { segment }, ecl, minVersion: version, maxVersion: version, boostEcl: boostEcl);
130+
return new List<QrCode> { qrCode };
131+
}
132+
133+
var overhead = (2 * 4 + numCharCountBits + 16 + 7) / 8; // 2 mode indicators + char count + structured append + byte alignment
134+
var maxSliceSize = numDataCodewords - overhead;
135+
int numSlices = (textBytes.Length + maxSliceSize - 1) / maxSliceSize;
136+
137+
var segments = QrSegment.MakeStructuredAppendSegments(textBytes, numSlices, considerUtf8Boundaries: true);
138+
if (segments.Max(s => s[1].NumChars) > maxSliceSize)
139+
{
140+
numSlices++;
141+
segments = QrSegment.MakeStructuredAppendSegments(textBytes, numSlices, considerUtf8Boundaries: true);
142+
}
143+
144+
if (numSlices > 16)
145+
{
146+
throw new DataTooLongException("Text is too long to fit in 16 QR codes with the given version and ECL");
147+
}
148+
149+
int balancedSliceSize = (textBytes.Length + numSlices - 1) / numSlices;
150+
return segments
151+
.Select(segmentList => EncodeSegments(segmentList, ecl, minVersion: version, maxVersion: version, boostEcl: boostEcl))
152+
.ToList();
153+
}
154+
94155
/// <summary>
95156
/// Creates a QR code representing the specified binary data using the specified error correction level.
96157
/// <para>
@@ -936,7 +997,7 @@ private int[] GetAlignmentPatternPositions()
936997
{
937998
if (Version == 1)
938999
{
939-
return new int[] { };
1000+
return Array.Empty<int>();
9401001
}
9411002
else
9421003
{
@@ -952,9 +1013,13 @@ private int[] GetAlignmentPatternPositions()
9521013
}
9531014
}
9541015

955-
// Returns the number of data bits that can be stored in a QR code of the given version number, after
956-
// all function modules are excluded. This includes remainder bits, so it might not be a multiple of 8.
957-
// The result is in the range [208, 29648]. This could be implemented as a 40-entry lookup table.
1016+
/// <summary>
1017+
/// Returns the number of data bits that can be stored in a QR code of the given version.
1018+
/// <para>
1019+
/// The returned number is after all function modules are excluded. This includes remainder bits,
1020+
/// so it might not be a multiple of 8. The result is in the range [208, 29648].
1021+
/// </para>
1022+
/// </summary>
9581023
private static int GetNumRawDataModules(int ver)
9591024
{
9601025
if (ver < MinVersion || ver > MaxVersion)
@@ -986,10 +1051,16 @@ private static int GetNumRawDataModules(int ver)
9861051
}
9871052

9881053

989-
// Returns the number of 8-bit data (i.e. not error correction) codewords contained in any
990-
// QR code of the given version number and error correction level, with remainder bits discarded.
991-
// This stateless pure function could be implemented as a (40*4)-cell lookup table.
992-
internal static int GetNumDataCodewords(int ver, Ecc ecl)
1054+
/// <summary>
1055+
/// Returns the number of 8-bit data codewords contained in a QR code of the given version number and error correction level.
1056+
/// <para>
1057+
/// The result is the net data capacity, without error correction data, and after discarding remainder bits.
1058+
/// </para>
1059+
/// </summary>
1060+
/// <param name="ver">The version number.</param>
1061+
/// <param name="ecl">The error correction level.</param>
1062+
/// <returns>The number of codewords.</returns>
1063+
public static int GetNumDataCodewords(int ver, Ecc ecl)
9931064
{
9941065
return GetNumRawDataModules(ver) / 8
9951066
- EccCodewordsPerBlock[ecl.Ordinal, ver]

QrCodeGenerator/QrSegment.cs

Lines changed: 128 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,48 @@ public static QrSegment MakeBytes(byte[] data)
8787
}
8888

8989

90+
/// <summary>
91+
/// Creates a segment representing the specified binary data
92+
/// encoded in byte mode. All input byte arrays are acceptable.
93+
/// <para>
94+
/// Any text string can be converted to UTF-8 bytes (using <c>Encoding.UTF8.GetBytes(str)</c>)
95+
/// and encoded as a byte mode segment.
96+
/// </para>
97+
/// </summary>
98+
/// <param name="data">The binary data to encode.</param>
99+
/// <returns>The created segment containing the specified data.</returns>
100+
/// <exception cref="ArgumentNullException"><c>data</c> is <c>null</c>.</exception>
101+
public static QrSegment MakeBytes(ArraySegment<byte> data)
102+
{
103+
Objects.RequireNonNull(data);
104+
var ba = new BitArray(0);
105+
foreach (var b in data)
106+
{
107+
ba.AppendBits(b, 8);
108+
}
109+
110+
return new QrSegment(Mode.Byte, data.Count, ba);
111+
}
112+
113+
114+
/// <summary>
115+
/// Creates a segment for the structured append header.
116+
/// </summary>
117+
/// <param name="parity">The parity value.</param>
118+
/// <param name="position">The position of the code within the sequence of codes (1 based).</param>
119+
/// <param name="total">The total number of codes in the sequence of codes.</param>
120+
/// <returns>The created segment containing the specified header.</returns>
121+
/// <exception cref="ArgumentNullException"><c>data</c> is <c>null</c>.</exception>
122+
private static QrSegment MakeStructuredAppend(byte parity, int position, int total)
123+
{
124+
var bitArray = new BitArray(0);
125+
bitArray.AppendBits((uint)position - 1, 4);
126+
bitArray.AppendBits((uint)total - 1, 4);
127+
bitArray.AppendBits(parity, 8);
128+
return new QrSegment(Mode.StructuredAppend, 0, bitArray);
129+
}
130+
131+
90132
/// <summary>
91133
/// Creates a segment representing the specified string of decimal digits.
92134
/// The segment is encoded in numeric mode.
@@ -195,6 +237,77 @@ public static List<QrSegment> MakeSegments(string text)
195237
}
196238

197239

240+
/// <summary>
241+
/// Creates the segments represeting the specified binary data,
242+
/// split into multiple QR codes (Structured Append).
243+
/// <para>
244+
/// The outer list represents the series of QR codes to be created.
245+
/// The inner lists contains the QR segments for each QR code.
246+
/// Each inner list contains at least two segments: the structured append segment and the binary data.
247+
/// </para>
248+
/// <para>
249+
/// The data is split into slices of similar size.
250+
/// </para>
251+
/// <para>
252+
/// If <paramref name="considerUtf8Boundaries"/> is set to <c>true</c>, the provided data is interpreted
253+
/// as UTF-8 encoded text and the data is split only at UTF-8 character boundaries, i.e. no split occurs
254+
/// in the middle of a multi-byte UTF-8 character sequence.
255+
/// </para>
256+
/// <para>
257+
/// The structured append segment specifies the number of QR codes (0-15), the position within
258+
/// those (0-15) and a parity which needs to be the same for all.
259+
/// </para>
260+
/// </summary>
261+
/// <param name="data">The binary data to encode in multiple QR codes</param>
262+
/// <param name="numberOfCodes">The number of codes to create.</param>
263+
/// <param name="considerUtf8Boundaries">If set to <c>true</c>, splits are made only at UTF-8 character boundaries.</param>
264+
/// <returns>A list of list of QR segments.</returns>
265+
public static List<List<QrSegment>> MakeStructuredAppendSegments(byte[] data, int numberOfCodes, bool considerUtf8Boundaries = false)
266+
{
267+
var result = new List<List<QrSegment>>();
268+
269+
byte parity = 0;
270+
foreach (var val in data)
271+
{
272+
parity ^= (byte)(val >> 8);
273+
}
274+
275+
var startOfSlice = 0;
276+
var length = (long)data.Length;
277+
for (int position = 1; position <= numberOfCodes; position += 1)
278+
{
279+
var endOfSlice = (int)(length * position / numberOfCodes);
280+
if (considerUtf8Boundaries)
281+
{
282+
endOfSlice = NextCharacterBoundary(data, endOfSlice);
283+
}
284+
if (startOfSlice == endOfSlice)
285+
{
286+
continue;
287+
}
288+
289+
result.Add(new List<QrSegment>
290+
{
291+
MakeStructuredAppend(parity, position, numberOfCodes),
292+
MakeBytes(new ArraySegment<byte>(data, startOfSlice, endOfSlice - startOfSlice))
293+
});
294+
295+
startOfSlice = endOfSlice;
296+
}
297+
298+
return result;
299+
}
300+
301+
private static int NextCharacterBoundary(byte[] data, int startIndex)
302+
{
303+
while (startIndex < data.Length && (data[startIndex] & 0xC0) == 0x80)
304+
{
305+
startIndex += 1;
306+
}
307+
return startIndex;
308+
}
309+
310+
198311
/// <summary>
199312
/// Creates a segment representing an Extended Channel Interpretation
200313
/// (ECI) designator with the specified assignment value.
@@ -333,9 +446,16 @@ public BitArray GetData()
333446
}
334447

335448

336-
// Calculates the number of bits needed to encode the given segments at the given version.
337-
// Returns a non-negative number if successful. Otherwise returns -1 if a segment has too
338-
// many characters to fit its length field, or the total bits exceeds int.MaxValue.
449+
/// <summary>
450+
/// Calculates the number of bits needed to encode the given segments.
451+
/// <para>
452+
/// Returns a non-negative number if successful. Otherwise returns -1 if a segment has too
453+
/// many characters to fit its length field, or the total bits exceeds int.MaxValue.
454+
/// </para>
455+
/// </summary>
456+
/// <param name="segments">The segements.</param>
457+
/// <param name="version">The version number.</param>
458+
/// <returns>The number of bits, or -1.</returns>
339459
internal static int GetTotalBits(List<QrSegment> segments, int version)
340460
{
341461
Objects.RequireNonNull(segments);
@@ -417,6 +537,11 @@ public sealed class Mode
417537
/// <value>ECI encoding mode.</value>
418538
public static readonly Mode Eci = new Mode(0x7, 0, 0, 0);
419539

540+
/// <summary>
541+
/// Structured append encoding mode.
542+
/// </summary>
543+
/// <value>Structured append encoding mode.</value>
544+
public static readonly Mode StructuredAppend = new Mode(0x3, 0, 0, 0);
420545

421546
/// <summary>
422547
/// Mode indicator value.

0 commit comments

Comments
 (0)