464 lines
17 KiB
C#
464 lines
17 KiB
C#
/*
|
|
* QR code generator library (.NET)
|
|
*
|
|
* Copyright (c) Manuel Bleichenbacher (MIT License)
|
|
* https://github.com/manuelbl/QrCodeGenerator
|
|
* Copyright (c) Project Nayuki (MIT License)
|
|
* https://www.nayuki.io/page/qr-code-generator-library
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
* IN THE SOFTWARE.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Diagnostics;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace zero.Rendering.QrCode;
|
|
|
|
/// <summary>
|
|
/// Represents a segment of character/binary/control data in a QR code symbol.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// The easiest way to deal with QR code segments is to call
|
|
/// <see cref="QrCode.EncodeText"/> or <see cref="QrCode.EncodeBinary"/>, and not
|
|
/// to use instances of this class directly. The mid-level way is to take the payload
|
|
/// data and call a static factory function such as <see cref="MakeNumeric(string)"/>.
|
|
/// The low-level way is to custom-make the bit array and call the
|
|
/// <see cref="QrSegment(Mode, int, BitArray)"/> constructor with appropriate values.
|
|
/// </para>
|
|
/// <para>
|
|
/// This segment class imposes no length restrictions, but QR codes have restrictions.
|
|
/// Even in the most favorable conditions, a QR code can only hold 7089 characters of data.
|
|
/// Any segment longer than this is meaningless for the purpose of generating QR codes.
|
|
/// </para>
|
|
/// <para>
|
|
/// This class can represent kanji mode segments, but provides no help in encoding them
|
|
/// - see <see cref="QrSegmentAdvanced"/> for full kanji support.
|
|
/// </para>
|
|
/// <para>
|
|
/// Instances of this class are immutable.
|
|
/// </para>
|
|
/// </remarks>
|
|
public class QrSegment
|
|
{
|
|
#region Static factory functions (mid level)
|
|
|
|
/// <summary>
|
|
/// Creates a segment representing the specified binary data
|
|
/// encoded in byte mode. All input byte arrays are acceptable.
|
|
/// <para>
|
|
/// Any text string can be converted to UTF-8 bytes (using <c>Encoding.UTF8.GetBytes(str)</c>)
|
|
/// and encoded as a byte mode segment.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <param name="data">The binary data to encode.</param>
|
|
/// <returns>The created segment containing the specified data.</returns>
|
|
/// <exception cref="ArgumentNullException"><c>data</c> is <c>null</c>.</exception>
|
|
public static QrSegment MakeBytes(byte[] data)
|
|
{
|
|
Objects.RequireNonNull(data);
|
|
var ba = new BitArray(0);
|
|
foreach (var b in data)
|
|
{
|
|
ba.AppendBits(b, 8);
|
|
}
|
|
|
|
return new QrSegment(Mode.Byte, data.Length, ba);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Creates a segment representing the specified string of decimal digits.
|
|
/// The segment is encoded in numeric mode.
|
|
/// </summary>
|
|
/// <param name="digits">The text to encode, consisting of digits from 0 to 9 only.</param>
|
|
/// <returns>The created segment containing the text.</returns>
|
|
/// <exception cref="ArgumentNullException"><c>digits</c> is <c>null</c>.</exception>
|
|
/// <exception cref="ArgumentOutOfRangeException"><c>digits</c> contains non-digit characters</exception>
|
|
public static QrSegment MakeNumeric(string digits)
|
|
{
|
|
Objects.RequireNonNull(digits);
|
|
if (!IsNumeric(digits))
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(digits), "String contains non-numeric characters");
|
|
}
|
|
|
|
var ba = new BitArray(0);
|
|
for (var i = 0; i < digits.Length;)
|
|
{
|
|
// Consume up to 3 digits per iteration
|
|
var n = Math.Min(digits.Length - i, 3);
|
|
ba.AppendBits(uint.Parse(digits.Substring(i, n)), n * 3 + 1);
|
|
i += n;
|
|
}
|
|
return new QrSegment(Mode.Numeric, digits.Length, ba);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Creates a segment representing the specified text string.
|
|
/// The segment is encoded in alphanumeric mode.
|
|
/// <para>
|
|
/// Allowed characters are: 0 to 9, A to Z (uppercase only), space,
|
|
/// dollar, percent, asterisk, plus, hyphen, period, slash, colon.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <param name="text">The text to encode, consisting of allowed characters only.</param>
|
|
/// <returns>The created segment containing the text.</returns>
|
|
/// <exception cref="ArgumentNullException"><c>text</c> is <c>null</c>.</exception>
|
|
/// <exception cref="ArgumentOutOfRangeException"><c>text</c> contains non-encodable characters.</exception>
|
|
public static QrSegment MakeAlphanumeric(string text)
|
|
{
|
|
Objects.RequireNonNull(text);
|
|
if (!IsAlphanumeric(text))
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(text), "String contains unencodable characters in alphanumeric mode");
|
|
}
|
|
|
|
var ba = new BitArray(0);
|
|
int i;
|
|
for (i = 0; i <= text.Length - 2; i += 2)
|
|
{
|
|
// Process groups of 2
|
|
var temp = (uint)AlphanumericCharset.IndexOf(text[i]) * 45;
|
|
temp += (uint)AlphanumericCharset.IndexOf(text[i + 1]);
|
|
ba.AppendBits(temp, 11);
|
|
}
|
|
if (i < text.Length) // 1 character remaining
|
|
{
|
|
ba.AppendBits((uint)AlphanumericCharset.IndexOf(text[i]), 6);
|
|
}
|
|
|
|
return new QrSegment(Mode.Alphanumeric, text.Length, ba);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Creates a list of zero or more segments representing the specified text string.
|
|
/// <para>
|
|
/// The text may contain the full range of Unicode characters.
|
|
/// </para>
|
|
/// <para>
|
|
/// The result may multiple segments with various encoding modes in order to minimize the length of the bit stream.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <param name="text">The text to be encoded.</param>
|
|
/// <returns>The created mutable list of segments representing the specified text.</returns>
|
|
/// <exception cref="ArgumentNullException"><c>text</c> is <c>null</c>.</exception>
|
|
/// <remarks>
|
|
/// The current implementation does not use multiple segments.
|
|
/// </remarks>
|
|
public static List<QrSegment> MakeSegments(string text)
|
|
{
|
|
Objects.RequireNonNull(text);
|
|
|
|
// Select the most efficient segment encoding automatically
|
|
var result = new List<QrSegment>();
|
|
if (text == "")
|
|
{
|
|
// Leave result empty
|
|
}
|
|
else if (IsNumeric(text))
|
|
{
|
|
result.Add(MakeNumeric(text));
|
|
}
|
|
else if (IsAlphanumeric(text))
|
|
{
|
|
result.Add(MakeAlphanumeric(text));
|
|
}
|
|
else
|
|
{
|
|
result.Add(MakeBytes(Encoding.UTF8.GetBytes(text)));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Creates a segment representing an Extended Channel Interpretation
|
|
/// (ECI) designator with the specified assignment value.
|
|
/// </summary>
|
|
/// <param name="assignVal">The ECI assignment number (see the AIM ECI specification).</param>
|
|
/// <returns>The created segment containing the data.</returns>
|
|
/// <exception cref="ArgumentOutOfRangeException"><c>assignVal</c>is outside the range [0, 10<sup>6</sup>).</exception>
|
|
public static QrSegment MakeEci(int assignVal)
|
|
{
|
|
var ba = new BitArray(0);
|
|
if (assignVal < 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(assignVal), "ECI assignment value out of range");
|
|
}
|
|
|
|
if (assignVal < 1 << 7)
|
|
{
|
|
ba.AppendBits((uint)assignVal, 8);
|
|
}
|
|
else if (assignVal < 1 << 14)
|
|
{
|
|
ba.AppendBits(2, 2);
|
|
ba.AppendBits((uint)assignVal, 14);
|
|
}
|
|
else if (assignVal < 1_000_000)
|
|
{
|
|
ba.AppendBits(6, 3);
|
|
ba.AppendBits((uint)assignVal, 21);
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(assignVal), "ECI assignment value out of range");
|
|
}
|
|
|
|
return new QrSegment(Mode.Eci, 0, ba);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests whether the specified string can be encoded as a segment in numeric mode.
|
|
/// <para>
|
|
/// A string is encodable iff each character is in the range "0" to "9".
|
|
/// </para>
|
|
/// </summary>
|
|
/// <param name="text">the string to test for encodability (not <c>null</c>)</param>
|
|
/// <returns><c>true</c> iff each character is in the range "0" to "9".</returns>
|
|
/// <exception cref="NullReferenceException">if the string is <c>null</c></exception>
|
|
/// <seealso cref="MakeNumeric(string)"/>
|
|
public static bool IsNumeric(string text)
|
|
{
|
|
return NumericRegex.IsMatch(text);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests whether the specified string can be encoded as a segment in alphanumeric mode.
|
|
/// <para>
|
|
/// A string is encodable iff each character is in the range "0" to "9", "A" to "Z" (uppercase only),
|
|
/// space, dollar, percent, asterisk, plus, hyphen, period, slash, colon.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <param name="text">the string to test for encodability (not <c>null</c>)</param>
|
|
/// <returns><c>true</c> iff each character is in the alphanumeric mode character set.</returns>
|
|
/// <exception cref="NullReferenceException">if the string is <c>null</c></exception>
|
|
/// <seealso cref="MakeAlphanumeric(string)"/>
|
|
public static bool IsAlphanumeric(string text)
|
|
{
|
|
return AlphanumericRegex.IsMatch(text);
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Instance fields
|
|
|
|
/// <summary>The encoding mode of this segment.</summary>
|
|
/// <value>Encoding mode.</value>
|
|
public Mode EncodingMode { get; }
|
|
|
|
/// <summary>
|
|
/// The length of this segment's unencoded data.
|
|
/// <para>
|
|
/// Measured in characters for numeric/alphanumeric/kanji mode,
|
|
/// bytes for byte mode, and 0 for ECI mode.
|
|
/// </para>
|
|
/// <para>
|
|
/// Different from the data's bit length.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <value>Length of the segment's unencoded data.</value>
|
|
public int NumChars { get; }
|
|
|
|
// The data bits of this segment. Not null. Accessed through GetData().
|
|
private readonly BitArray _data;
|
|
|
|
#endregion
|
|
|
|
|
|
#region Constructor (low level)
|
|
|
|
/// <summary>
|
|
/// Initializes a QR code segment with the specified attributes and data.
|
|
/// <para>
|
|
/// The character count <paramref name="numChars"/> must agree with the mode and the bit array length,
|
|
/// but the constraint isn't checked. The specified bit array is cloned.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <param name="mode">The segment mode used to encode this segment.</param>
|
|
/// <param name="numChars">The data length in characters or bytes (depending on the segment mode).</param>
|
|
/// <param name="data">The data bits.</param>
|
|
/// <exception cref="ArgumentNullException"><paramref name="mode"/> or <paramref name="data"/> is <c>null</c>.</exception>
|
|
/// <exception cref="ArgumentOutOfRangeException"><paramref name="numChars"/> is negative.</exception>
|
|
public QrSegment(Mode mode, int numChars, BitArray data)
|
|
{
|
|
EncodingMode = Objects.RequireNonNull(mode);
|
|
Objects.RequireNonNull(data);
|
|
if (numChars < 0)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(numChars), "Invalid value");
|
|
}
|
|
|
|
NumChars = numChars;
|
|
_data = (BitArray)data.Clone(); // Make defensive copy
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Methods
|
|
|
|
/// <summary>
|
|
/// Returns a copy of this segment's data bits.
|
|
/// </summary>
|
|
/// <returns>A copy of the data bits.</returns>
|
|
public BitArray GetData()
|
|
{
|
|
return (BitArray)_data.Clone(); // Make defensive copy
|
|
}
|
|
|
|
|
|
// Calculates the number of bits needed to encode the given segments at the given version.
|
|
// Returns a non-negative number if successful. Otherwise returns -1 if a segment has too
|
|
// many characters to fit its length field, or the total bits exceeds int.MaxValue.
|
|
internal static int GetTotalBits(List<QrSegment> segments, int version)
|
|
{
|
|
Objects.RequireNonNull(segments);
|
|
long result = 0;
|
|
foreach (var seg in segments)
|
|
{
|
|
Objects.RequireNonNull(seg);
|
|
var ccBits = seg.EncodingMode.NumCharCountBits(version);
|
|
if (seg.NumChars >= 1 << ccBits)
|
|
{
|
|
return -1; // The segment's length doesn't fit the field's bit width
|
|
}
|
|
|
|
result += 4L + ccBits + seg._data.Length;
|
|
if (result > int.MaxValue)
|
|
{
|
|
return -1; // The sum will overflow an int type
|
|
}
|
|
}
|
|
return (int)result;
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Constants
|
|
|
|
// Describes precisely all strings that are encodable in numeric mode.
|
|
private static readonly Regex NumericRegex = new Regex("^[0-9]*$", RegexOptions.Compiled);
|
|
|
|
// Describes precisely all strings that are encodable in alphanumeric mode.
|
|
private static readonly Regex AlphanumericRegex = new Regex("^[A-Z0-9 $%*+./:-]*$", RegexOptions.Compiled);
|
|
|
|
|
|
// The set of all legal characters in alphanumeric mode, where
|
|
// each character value maps to the index in the string.
|
|
internal const string AlphanumericCharset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
|
|
|
|
#endregion
|
|
|
|
|
|
#region Public helper enumeration
|
|
|
|
/// <summary>
|
|
/// Segment encoding mode.
|
|
/// <para>
|
|
/// Describes how text or binary data is encoded into bits.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class Mode
|
|
{
|
|
/// <summary>
|
|
/// Numeric encoding mode.
|
|
/// </summary>
|
|
/// <value>Numeric encoding mode.</value>
|
|
public static readonly Mode Numeric = new Mode(0x1, 10, 12, 14);
|
|
|
|
/// <summary>
|
|
/// Alphanumeric encoding mode.
|
|
/// </summary>
|
|
/// <value>Alphanumeric encoding mode.</value>
|
|
public static readonly Mode Alphanumeric = new Mode(0x2, 9, 11, 13);
|
|
|
|
/// <summary>
|
|
/// Byte encoding mode.
|
|
/// </summary>
|
|
/// <value>Byte encoding mode.</value>
|
|
public static readonly Mode Byte = new Mode(0x4, 8, 16, 16);
|
|
|
|
/// <summary>
|
|
/// Kanji encoding mode.
|
|
/// </summary>
|
|
/// <value>Kanji encoding mode.</value>
|
|
public static readonly Mode Kanji = new Mode(0x8, 8, 10, 12);
|
|
|
|
/// <summary>
|
|
/// ECI encoding mode.
|
|
/// </summary>
|
|
/// <value>ECI encoding mode.</value>
|
|
public static readonly Mode Eci = new Mode(0x7, 0, 0, 0);
|
|
|
|
|
|
/// <summary>
|
|
/// Mode indicator value.
|
|
/// <para>
|
|
/// 4 bit value in the QR segment header indicating the encoding mode.
|
|
/// </para>
|
|
/// </summary>
|
|
/// <value>Mode indicator value</value>
|
|
internal uint ModeBits { get; }
|
|
|
|
|
|
/// <summary>
|
|
/// Array of character count bit length.
|
|
/// <para>
|
|
/// Number of bits for character count in QR segment header.
|
|
/// The three array values apply to versions 0 to 9, 10 to 26 and 27 to 40
|
|
/// respectively. All array values are in the range [0, 16].
|
|
/// </para>
|
|
/// </summary>
|
|
/// <value>Array of character count bit length</value>
|
|
private int[] NumBitsCharCount { get; }
|
|
|
|
|
|
/// <summary>
|
|
/// Returns the bit length of the character count in the QR segment header
|
|
/// for the specified QR code version. The result is in the range [0, 16].
|
|
/// </summary>
|
|
/// <param name="ver">the QR code version (between 1 and 40)</param>
|
|
/// <returns></returns>
|
|
internal int NumCharCountBits(int ver)
|
|
{
|
|
Debug.Assert(QrCode.MIN_VERSION <= ver && ver <= QrCode.MAX_VERSION);
|
|
return NumBitsCharCount[(ver + 7) / 17];
|
|
}
|
|
|
|
// private constructor to initializes the constants
|
|
private Mode(uint modeBits, params int[] numBitsCharCount)
|
|
{
|
|
ModeBits = modeBits;
|
|
NumBitsCharCount = numBitsCharCount;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
} |