Skip to content
Open
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
54 changes: 47 additions & 7 deletions QrCodeGenerator/QrCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,41 @@ public static QrCode EncodeText(string text, Ecc ecl)
return EncodeSegments(segments, ecl);
}

/// <summary>
/// Creates a Series of QR codes representing the specified text using the specified error correction level
/// as structured append. Structured append requires an additional QrSegment at the start specifying the
/// number of codes (0-15), the position within those (0-15) and a parity which needs to be the same for all.
/// This method tries to split a given text optimally, meaning all QrCodes end up encoding a similar number of
/// characters.
/// </summary>
/// <param name="text">The text to be encoded. The full range of Unicode characters may be used.</param>
/// <param name="ecl">The minimum error correction level to use.</param>
/// <param name="version">The version to use, default is 29</param>
/// <param name="boostEcl">Whether ecl should be upgraded if possible, default is false</param>
/// <returns>A list of created QR code instance representing the specified text split into QR code of the size of the specified version</returns>
/// <exception cref="ArgumentNullException"><paramref name="text"/> or <paramref name="ecl"/> is <c>null</c>.</exception>
/// <exception cref="DataTooLongException">The text is too long to fit in the largest QR code size (version)
/// at the specified error correction level.</exception>
public static List<QrCode> EncodeTextAsStructuredAppend(string text, Ecc ecl, int version = 29, bool boostEcl = false)
{
Objects.RequireNonNull(text);
Objects.RequireNonNull(ecl);
var maxSplitSize = GetNumDataCodewords(version, ecl) - 5;
int numSegments = (text.Length + maxSplitSize - 1) / maxSplitSize;
int idealSplit = text.Length / numSegments + (text.Length / numSegments == maxSplitSize ? 0 : 1);
var segments = QrSegment.MakeStructuredAppendSegments(text, idealSplit);
if (segments.Count > 16)
{
throw new DataTooLongException("Text does not fit within 16 codes of the chosen version");
}
var result = new List<QrCode>(segments.Count);
foreach (var seg in segments)
{
result.Add(EncodeSegments(seg, ecl, minVersion: version, maxVersion: version, boostEcl: boostEcl));
}
return result;
}

/// <summary>
/// Creates a QR code representing the specified binary data using the specified error correction level.
/// <para>
Expand Down Expand Up @@ -196,7 +231,6 @@ public static QrCode EncodeSegments(List<QrSegment> segments, Ecc ecl, int minVe

throw new DataTooLongException(msg);
}
Debug.Assert(dataUsedBits != -1);

// Increase the error correction level while the data still fits in the current version number
foreach (var newEcl in Ecc.AllValues)
Expand All @@ -212,14 +246,19 @@ public static QrCode EncodeSegments(List<QrSegment> segments, Ecc ecl, int minVe
foreach (var seg in segments)
{
ba.AppendBits(seg.EncodingMode.ModeBits, 4);
ba.AppendBits((uint)seg.NumChars, seg.EncodingMode.NumCharCountBits(version));
ba.AppendData(seg.GetData());
if (seg.EncodingMode.IsStructuredAppend())
{
ba.AppendBits((uint) seg.EncodingMode.AppendMode.SequenceIndicator, 4);
ba.AppendBits((uint) seg.EncodingMode.AppendMode.SequenceTotal, 4);
ba.AppendBits(seg.EncodingMode.AppendMode.Parity, 8);
} else {
ba.AppendBits((uint)seg.NumChars, seg.EncodingMode.NumCharCountBits(version));
ba.AppendData(seg.GetData());
}
}
Debug.Assert(ba.Length == dataUsedBits);

// Add terminator and pad up to a byte if applicable
var dataCapacityBits = GetNumDataCodewords(version, ecl) * 8;
Debug.Assert(ba.Length <= dataCapacityBits);
ba.AppendBits(0, Math.Min(4, dataCapacityBits - ba.Length));
ba.AppendBits(0, (8 - ba.Length % 8) % 8);
Debug.Assert(ba.Length % 8 == 0);
Expand All @@ -244,6 +283,8 @@ public static QrCode EncodeSegments(List<QrSegment> segments, Ecc ecl, int minVe
return new QrCode(version, ecl, dataCodewords, mask);
}



#endregion


Expand Down Expand Up @@ -355,7 +396,6 @@ public QrCode(int version, Ecc ecl, byte[] dataCodewords, int mask = -1)
ApplyMask(i); // Undoes the mask due to XOR
}
}
Debug.Assert(0 <= mask && mask <= 7);
Mask = mask;
ApplyMask((uint)mask); // Apply the final choice of mask
DrawFormatBits((uint)mask); // Overwrite old format bits
Expand Down Expand Up @@ -759,7 +799,7 @@ private void DrawCodewords(byte[] data)
// QR code needs exactly one (not zero, two, etc.) mask applied.
private void ApplyMask(uint mask)
{
if (mask < 0 || mask > 7)
if (mask > 7)
{
throw new ArgumentOutOfRangeException(nameof(mask), "Mask value out of range");
}
Expand Down
96 changes: 90 additions & 6 deletions QrCodeGenerator/QrSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

Expand Down Expand Up @@ -87,6 +88,20 @@ public static QrSegment MakeBytes(byte[] data)
}


/// <summary>
/// Creates a segment representing only the header of the structured append part
/// </summary>
/// <param name="parity">The parity that was calculated over the whole content</param>
/// <param name="seq">The sequence number of the current segment</param>
/// <param name="total">The number of sequences (QR codes) in total</param>
/// <returns>The created segment containing the specified header.</returns>
/// <exception cref="ArgumentNullException"><c>data</c> is <c>null</c>.</exception>
private static QrSegment MakeStructuredAppend(byte parity, int seq, int total)
{
return new QrSegment(Mode.StructuredAppend(parity, seq, total), 0, new BitArray(0));
}


/// <summary>
/// Creates a segment representing the specified string of decimal digits.
/// The segment is encoded in numeric mode.
Expand Down Expand Up @@ -194,6 +209,41 @@ public static List<QrSegment> MakeSegments(string text)
return result;
}


/// <summary>
/// Creates a series of pairs of QrSegments, each pair consisting of a Structured Append segment which only
/// contains metadata and a byte segment containing the data.
/// Structured append requires an additional QrSegment at the start specifying the
/// number of codes (0-15), the position within those (0-15) and a parity which needs to be the same for all.
/// Those are computed within this method.
/// </summary>
/// <param name="text">The text to encode as structured append, split</param>
/// <param name="splitSize">The size that each split should have</param>
/// <returns>A List of future QR codes, which each contain (at least) two segments: the structured append header and the binary data</returns>
public static List<List<QrSegment>> MakeStructuredAppendSegments(string text, int splitSize)
{
var result = new List<List<QrSegment>>();
byte parity = 0;
foreach (char val in text)
parity ^= (val <= 255) ? (byte)val : (byte)((byte)val ^ (byte)(val >> 8));
var allBytes = Encoding.UTF8.GetBytes(text);

int offset = 0;
int i = 0;
while (offset < allBytes.Length) {
int nextSegmentLength = offset + splitSize > allBytes.Length ? allBytes.Length - offset : splitSize;
var pair = new List<QrSegment>(2)
{
QrSegment.MakeStructuredAppend(parity, i, (text.Length + splitSize - 1) / splitSize - 1),
QrSegment.MakeBytes(new ArraySegment<byte>(allBytes, offset, nextSegmentLength).ToArray())
};
result.Add(pair);
offset += splitSize;
i++;
}
return result;
}


/// <summary>
/// Creates a segment representing an Extended Channel Interpretation
Expand Down Expand Up @@ -343,6 +393,11 @@ internal static int GetTotalBits(List<QrSegment> segments, int version)
foreach (var seg in segments)
{
Objects.RequireNonNull(seg);
if (seg.EncodingMode.IsStructuredAppend())
{
result += 20;
continue;
}
var ccBits = seg.EncodingMode.NumCharCountBits(version);
if (seg.NumChars >= 1 << ccBits)
{
Expand Down Expand Up @@ -391,31 +446,40 @@ public sealed class Mode
/// Numeric encoding mode.
/// </summary>
/// <value>Numeric encoding mode.</value>
public static readonly Mode Numeric = new Mode(0x1, 10, 12, 14);
public static readonly Mode Numeric = new Mode(0x1, null, 10, 12, 14);

/// <summary>
/// Alphanumeric encoding mode.
/// </summary>
/// <value>Alphanumeric encoding mode.</value>
public static readonly Mode Alphanumeric = new Mode(0x2, 9, 11, 13);
public static readonly Mode Alphanumeric = new Mode(0x2, null, 9, 11, 13);

/// <summary>
/// Byte encoding mode.
/// </summary>
/// <value>Byte encoding mode.</value>
public static readonly Mode Byte = new Mode(0x4, 8, 16, 16);
public static readonly Mode Byte = new Mode(0x4, null, 8, 16, 16);

/// <summary>
/// Kanji encoding mode.
/// </summary>
/// <value>Kanji encoding mode.</value>
public static readonly Mode Kanji = new Mode(0x8, 8, 10, 12);
public static readonly Mode Kanji = new Mode(0x8, null, 10, 12);

/// <summary>
/// ECI encoding mode.
/// </summary>
/// <value>ECI encoding mode.</value>
public static readonly Mode Eci = new Mode(0x7, 0, 0, 0);
public static readonly Mode Eci = new Mode(0x7, null, 0, 0);

/// <summary>
/// StructuredAppend encoding mode.
/// </summary>
/// <value>uses Alphanumeric encoding mode.</value>
public static Mode StructuredAppend(byte parity, int seq, int total)
{
return new Mode(0x3, new StructuredAppendMode(parity, seq, total), 8, 16, 16);
}


/// <summary>
Expand All @@ -439,6 +503,8 @@ public sealed class Mode
/// <value>Array of character count bit length</value>
private int[] NumBitsCharCount { get; }

internal StructuredAppendMode AppendMode { get; }


/// <summary>
/// Returns the bit length of the character count in the QR segment header
Expand All @@ -452,11 +518,29 @@ internal int NumCharCountBits(int ver)
return NumBitsCharCount[(ver + 7) / 17];
}

internal bool IsStructuredAppend()
{
return ModeBits == 0x3;
}

// private constructor to initializes the constants
private Mode(uint modeBits, params int[] numBitsCharCount)
private Mode(uint modeBits, StructuredAppendMode sMode, params int[] numBitsCharCount)
{
ModeBits = modeBits;
NumBitsCharCount = numBitsCharCount;
AppendMode = sMode;
}

internal sealed class StructuredAppendMode
{
internal byte Parity {get;}
internal int SequenceIndicator {get;}
internal int SequenceTotal {get;}
internal StructuredAppendMode (byte parity, int sequenceIndicator, int sequenceTotal) {
Parity = parity;
SequenceIndicator = sequenceIndicator;
SequenceTotal = sequenceTotal;
}
}
}

Expand Down
29 changes: 29 additions & 0 deletions QrCodeGeneratorTest/QrCodeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,34 @@ public void TestQrCode(QrCodeTestCase testCase)
Assert.Equal(testCase.EffectiveMask, qrCode.Mask);
Assert.Equal(testCase.ExpectedModules, TestHelper.ToStringArray(qrCode));
}

[Fact]
public void TestStructuredAppendQrCode()
{
var qrCodes = EncodeTextAsStructuredAppend(
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores.",
Ecc.Medium, 29);
Assert.Single(qrCodes);
}


[Fact]
public void TestStructuredAppendQrCodeFullSize1262()
{
var qrCodes = EncodeTextAsStructuredAppend(
"A wonderful serenity has taken possession of my entire soul, like these sweet mornings of spring which I enjoy with my whole heart. I am alone, and feel the charm of existence in this spot, which was created for the bliss of souls like mine. I am so happy, my dear friend, so absorbed in the exquisite sense of mere tranquil existence, that I neglect my talents. I should be incapable of drawing a single stroke at the present moment; and yet I feel that I never was a greater artist than now. When, while the lovely valley teems with vapour around me, and the meridian sun strikes the upper surface of the impenetrable foliage of my trees, and but a few stray gleams steal into the inner sanctuary, I throw myself down among the tall grass by the trickling stream; and, as I lie close to the earth, a thousand unknown plants are noticed by me: when I hear the buzz of the little world among the stalks, and grow familiar with the countless indescribable forms of the insects and flies, then I feel the presence of the Almighty, who formed us in his own image, and the breath of that universal love which bears and sustains us, as it floats around us in an eternity of bliss; and then, my friend, when darkness overspreads my eyes, and heaven and earth seem to d",Ecc.Medium);
Assert.Single(qrCodes);
}

[Fact]
public void TestStructuredAppendQrCodeSplit2()
{
var qrCodes = EncodeTextAsStructuredAppend(
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores. 123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890",
Ecc.Medium, 29);
Assert.Equal(2, qrCodes.Count);
}


}
}