Files
mixtape/zero/Rendering/QrCode/QrSegment.cs
T
2022-12-09 14:40:18 +01:00

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
}