diff --git a/config.lock.yaml b/config.lock.yaml index 39e937c..bb36918 100644 --- a/config.lock.yaml +++ b/config.lock.yaml @@ -13,7 +13,7 @@ no_screenshot: 9ca2a492ff12e5179583a1fa015bf0843382b866 optional: 71c638891ce4f2aff35c7387727989f31f9d877d photo_view: a13ca2fc387a3fb1276126959e092c44d0029987 pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd -qr: 5fa01fcccd6121b906dc7df4fffa9fa22ca94f75 +qr: 7b1e9665ca976f484e7975356cf26fc7a0ccf02e qr_flutter: d5e7206396105d643113618290bbcc755d05f492 restart_app: 12339f63bf8e9631e619c4f9f6b4e013fa324715 x25519: ecb1d357714537bba6e276ef45f093846d4beaee diff --git a/qr/lib/qr.dart b/qr/lib/qr.dart index db99aa2..0565eb5 100644 --- a/qr/lib/qr.dart +++ b/qr/lib/qr.dart @@ -1,9 +1,5 @@ export 'src/bit_buffer.dart'; -export 'src/byte.dart'; -export 'src/eci.dart'; -export 'src/ecivalue.dart'; export 'src/error_correct_level.dart'; export 'src/input_too_long_exception.dart'; -export 'src/mode.dart'; export 'src/qr_code.dart'; export 'src/qr_image.dart'; diff --git a/qr/lib/src/bit_buffer.dart b/qr/lib/src/bit_buffer.dart index 9248490..3848f4e 100644 --- a/qr/lib/src/bit_buffer.dart +++ b/qr/lib/src/bit_buffer.dart @@ -1,78 +1,34 @@ -/// A growable sequence of bits. -/// -/// Used internally to construct the data bit stream for a QR code. -class QrBitBuffer extends Iterable { - final _buffer = []; +import 'dart:collection'; + +class QrBitBuffer extends Object with ListMixin { + final List _buffer; int _length = 0; - QrBitBuffer(); + QrBitBuffer() : _buffer = []; @override - int get length => _length; + void operator []=(int index, bool value) => + throw UnsupportedError('cannot change'); @override - Iterator get iterator => _QrBitBufferIterator(this); - bool operator [](int index) { final bufIndex = index ~/ 8; return ((_buffer[bufIndex] >> (7 - index % 8)) & 1) == 1; } + @override + int get length => _length; + + @override + set length(int value) => throw UnsupportedError('Cannot change'); + int getByte(int index) => _buffer[index]; void put(int number, int length) { - if (length == 0) return; - - var bitIndex = _length; - final endBitIndex = bitIndex + length; - - // Ensure capacity - final neededBytes = (endBitIndex + 7) >> 3; // (endBitIndex + 7) ~/ 8 - while (_buffer.length < neededBytes) { - _buffer.add(0); + for (var i = 0; i < length; i++) { + final bit = ((number >> (length - i - 1)) & 1) == 1; + putBit(bit); } - - // Optimization for byte-aligned writes of 8 bits (common case) - if (length == 8 && (bitIndex & 7) == 0 && number >= 0 && number <= 255) { - _buffer[bitIndex >> 3] = number; - _length = endBitIndex; - return; - } - - // Generic chunked write - var bitsLeft = length; - - while (bitsLeft > 0) { - final bufIndex = bitIndex >> 3; - final leftBitIndex = bitIndex & 7; - final available = 8 - leftBitIndex; - final bitsToWrite = bitsLeft < available ? bitsLeft : available; - - // Extract the 'bitsToWrite' most significant bits from 'number' - // Shift number right to move target bits to bottom - // Mask them - // Then allocate them to the byte buffer - - final shift = bitsLeft - bitsToWrite; - final bits = (number >> shift) & ((1 << bitsToWrite) - 1); - - // Setup position in byte. - // We want to write 'bits' starting at 'leftBitIndex'. - // So we shift 'bits' left by (available - bitsToWrite)? - // No, `leftBitIndex` is 0-7. 0 is MSB (0x80). - // If leftBitIndex is 0, we write starting at 0x80. - // If bitsToWrite is 8, we write 0xFF. - // If 4 bits, we write 0xF0. - // formula: bits << (8 - leftBitIndex - bitsToWrite) - - final posShift = 8 - leftBitIndex - bitsToWrite; - _buffer[bufIndex] |= bits << posShift; - - bitsLeft -= bitsToWrite; - bitIndex += bitsToWrite; - } - - _length = endBitIndex; } void putBit(bool bit) { @@ -87,28 +43,4 @@ class QrBitBuffer extends Iterable { _length++; } - - List getRange(int start, int end) { - final list = []; - for (var i = start; i < end; i++) { - list.add(this[i]); - } - return list; - } -} - -class _QrBitBufferIterator implements Iterator { - final QrBitBuffer _buffer; - int _currentIndex = -1; - - _QrBitBufferIterator(this._buffer); - - @override - bool get current => _buffer[_currentIndex]; - - @override - bool moveNext() { - _currentIndex++; - return _currentIndex < _buffer.length; - } } diff --git a/qr/lib/src/byte.dart b/qr/lib/src/byte.dart index 27aeddf..560644a 100644 --- a/qr/lib/src/byte.dart +++ b/qr/lib/src/byte.dart @@ -2,47 +2,17 @@ import 'dart:convert'; import 'dart:typed_data'; import 'bit_buffer.dart'; -import 'eci.dart'; -import 'mode.dart'; +import 'mode.dart' as qr_mode; -/// A piece of data to be encoded in a QR code. -/// -/// Use [toDatums] to parse a string into optimal segments. abstract class QrDatum { - QrMode get mode; + int get mode; int get length; void write(QrBitBuffer buffer); - - /// Parses [data] into a list of [QrDatum] segments, optimizing for the - /// most efficient encoding modes (Numeric, Alphanumeric, Byte). - /// - /// Automatically handles UTF-8 characters by using [QrEci] and [QrByte] - /// segments if necessary. - static List toDatums(String data) { - if (QrNumeric.validationRegex.hasMatch(data)) { - return [QrNumeric.fromString(data)]; - } - if (QrAlphaNumeric.validationRegex.hasMatch(data)) { - return [QrAlphaNumeric.fromString(data)]; - } - // Default to byte mode for other characters - // Check if we need ECI (if there are chars > 255) - // Actually, standard ISO-8859-1 is 0-255. - // Emojis and other UTF-8 chars will definitely trigger this. - final hasNonLatin1 = data.codeUnits.any((c) => c > 255); - if (hasNonLatin1) { - return [QrEci(26), QrByte(data)]; // UTF-8 - } - return [QrByte(data)]; - } } -/// Represents data encoded in Byte mode (8-bit). -/// -/// Supports ISO-8859-1 and UTF-8 (when preceded by an ECI segment). class QrByte implements QrDatum { @override - final QrMode mode = QrMode.byte; + final int mode = qr_mode.mode8bitByte; final Uint8List _data; factory QrByte(String input) => @@ -64,20 +34,13 @@ class QrByte implements QrDatum { } } -/// Encodes numeric data (digits 0-9). -/// -/// Compresses 3 digits into 10 bits. -/// Most efficient mode for decimal numbers. +/// Encodes numbers (0-9) 10 bits per 3 digits. class QrNumeric implements QrDatum { static final RegExp validationRegex = RegExp(r'^[0-9]+$'); factory QrNumeric.fromString(String numberString) { if (!validationRegex.hasMatch(numberString)) { - throw ArgumentError.value( - numberString, - 'numberString', - 'string can only contain digits 0-9', - ); + throw ArgumentError('string can only contain digits 0-9'); } final newList = Uint8List(numberString.length); var count = 0; @@ -92,7 +55,7 @@ class QrNumeric implements QrDatum { final Uint8List _data; @override - final QrMode mode = QrMode.numeric; + final int mode = qr_mode.modeNumber; @override void write(QrBitBuffer buffer) { @@ -119,10 +82,7 @@ class QrNumeric implements QrDatum { int get length => _data.length; } -/// Encodes alphanumeric data (uppercase letters, digits, and specific symbols). -/// -/// Supported characters: 0-9, A-Z, space, $, %, *, +, -, ., /, : -/// Compresses 2 characters into 11 bits. +/// Encodes numbers (0-9) 10 bits per 3 digits. class QrAlphaNumeric implements QrDatum { static const alphaNumTable = r'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'; // Note: '-' anywhere in this string is a range character. @@ -142,10 +102,9 @@ class QrAlphaNumeric implements QrDatum { factory QrAlphaNumeric.fromString(String alphaNumeric) { if (!alphaNumeric.contains(validationRegex)) { - throw ArgumentError.value( - alphaNumeric, - 'alphaNumeric', - 'String does not contain valid ALPHA-NUM character set', + throw ArgumentError( + 'String does not contain valid ALPHA-NUM ' + 'character set: $alphaNumeric', ); } return QrAlphaNumeric._(alphaNumeric); @@ -154,7 +113,7 @@ class QrAlphaNumeric implements QrDatum { QrAlphaNumeric._(this._string); @override - final QrMode mode = QrMode.alphaNumeric; + final int mode = qr_mode.modeAlphaNum; @override void write(QrBitBuffer buffer) { diff --git a/qr/lib/src/eci.dart b/qr/lib/src/eci.dart deleted file mode 100644 index bb939c9..0000000 --- a/qr/lib/src/eci.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'bit_buffer.dart'; -import 'byte.dart'; - -import 'mode.dart'; - -/// Extended Channel Interpretation (ECI) mode data. -/// -/// Use this to specify a different character encoding for the following data. -class QrEci implements QrDatum { - final int value; - - factory QrEci(int value) { - if (value < 0 || value > 999999) { - throw RangeError.range(value, 0, 999999, 'value'); - } - return QrEci._(value); - } - - QrEci._(this.value); - - @override - QrMode get mode => QrMode.eci; - - @override - int get length => 0; // ECI segments do not have a length field - - @override - void write(QrBitBuffer buffer) { - if (value < 128) { - // 0xxxxxxx - buffer.put(value, 8); - } else if (value < 16384) { - // 10xxxxxx xxxxxxxx - buffer.put(0x8000 | value, 16); - } else { - // 110xxxxx xxxxxxxx xxxxxxxx - buffer.put(0xC00000 | value, 24); - } - } -} diff --git a/qr/lib/src/ecivalue.dart b/qr/lib/src/ecivalue.dart deleted file mode 100644 index b5e9e89..0000000 --- a/qr/lib/src/ecivalue.dart +++ /dev/null @@ -1,87 +0,0 @@ -/// ECI value for QR Codes. -/// -/// This extension type provides constants for common ECI values. -/// -/// See: https://github.com/zxing/zxing/blob/master/core/src/main/java/com/google/zxing/common/CharacterSetECI.java -extension type const QrEciValue(int value) implements int { - /// ISO-8859-1 (Latin-1). Default encoding. - static const iso8859_1 = QrEciValue(3); - - /// ISO-8859-2 (Latin-2). - static const iso8859_2 = QrEciValue(4); - - /// ISO-8859-3 (Latin-3). - static const iso8859_3 = QrEciValue(5); - - /// ISO-8859-4 (Latin-4). - static const iso8859_4 = QrEciValue(6); - - /// ISO-8859-5 (Latin/Cyrillic). - static const iso8859_5 = QrEciValue(7); - - /// ISO-8859-6 (Latin/Arabic). - static const iso8859_6 = QrEciValue(8); - - /// ISO-8859-7 (Latin/Greek). - static const iso8859_7 = QrEciValue(9); - - /// ISO-8859-8 (Latin/Hebrew). - static const iso8859_8 = QrEciValue(10); - - /// ISO-8859-9 (Latin-5). - static const iso8859_9 = QrEciValue(11); - - /// ISO-8859-10 (Latin-6). - static const iso8859_10 = QrEciValue(12); - - /// ISO-8859-11 (Latin/Thai). - static const iso8859_11 = QrEciValue(13); - - /// ISO-8859-13 (Latin-7). - static const iso8859_13 = QrEciValue(15); - - /// ISO-8859-14 (Latin-8). - static const iso8859_14 = QrEciValue(16); - - /// ISO-8859-15 (Latin-9). - static const iso8859_15 = QrEciValue(17); - - /// ISO-8859-16 (Latin-10). - static const iso8859_16 = QrEciValue(18); - - /// Shift JIS. - static const shiftJis = QrEciValue(20); - - /// Windows-1250 (Latin-2). - static const windows1250 = QrEciValue(21); - - /// Windows-1251 (Cyrillic). - static const windows1251 = QrEciValue(22); - - /// Windows-1252 (Latin-1). - static const windows1252 = QrEciValue(23); - - /// Windows-1256 (Arabic). - static const windows1256 = QrEciValue(24); - - /// UTF-16 (Big Endian). - static const utf16BE = QrEciValue(25); - - /// UTF-8. - static const utf8 = QrEciValue(26); - - /// US-ASCII. - static const ascii = QrEciValue(27); - - /// Big5. - static const big5 = QrEciValue(28); - - /// GB 2312. - static const gb2312 = QrEciValue(29); - - /// EUC-KR. - static const eucKr = QrEciValue(30); - - /// GBK. - static const gbk = QrEciValue(31); -} diff --git a/qr/lib/src/error_correct_level.dart b/qr/lib/src/error_correct_level.dart index 8652a29..79c6f11 100644 --- a/qr/lib/src/error_correct_level.dart +++ b/qr/lib/src/error_correct_level.dart @@ -1,27 +1,20 @@ -/// QR Code error correction level. -/// -/// Recover capacity: -/// * [low] : ~7% -/// * [medium] : ~15% -/// * [quartile] : ~25% -/// * [high] : ~30% -enum QrErrorCorrectLevel { - // NOTE: the order here MATTERS. - // The index maps to the QR standard. +// ignore: avoid_classes_with_only_static_members +class QrErrorCorrectLevel { + static const int L = 1; + static const int M = 0; + static const int Q = 3; + static const int H = 2; - /// Level M (Medium) ~15% error correction. - medium(15), + // thesee *are* in order of lowest to highest quality...I think + // all I know for sure: you can create longer messages w/ item N than N+1 + // I assume this correcsponds to more error correction for N+1 + static const List levels = [L, M, Q, H]; - /// Level L (Low) ~7% error correction. - low(7), - - /// Level H (High) ~30% error correction. - high(30), - - /// Level Q (Quartile) ~25% error correction. - quartile(25); - - final int recoveryRate; - - const QrErrorCorrectLevel(this.recoveryRate); + static String getName(int level) => switch (level) { + L => 'Low', + M => 'Medium', + Q => 'Quartile', + H => 'High', + _ => throw ArgumentError('level $level not supported'), + }; } diff --git a/qr/lib/src/mask_pattern.dart b/qr/lib/src/mask_pattern.dart index a20637f..f6a42f8 100644 --- a/qr/lib/src/mask_pattern.dart +++ b/qr/lib/src/mask_pattern.dart @@ -1,25 +1,8 @@ -enum QrMaskPattern { - pattern000(_check000), - pattern001(_check001), - pattern010(_check010), - pattern011(_check011), - pattern100(_check100), - pattern101(_check101), - pattern110(_check110), - pattern111(_check111); - - final bool Function(int i, int j) _check; - - const QrMaskPattern(this._check); - - bool check(int i, int j) => _check(i, j); -} - -bool _check000(int i, int j) => (i + j).isEven; -bool _check001(int i, int j) => i.isEven; -bool _check010(int i, int j) => j % 3 == 0; -bool _check011(int i, int j) => (i + j) % 3 == 0; -bool _check100(int i, int j) => ((i ~/ 2) + (j ~/ 3)).isEven; -bool _check101(int i, int j) => ((i * j) % 2 + (i * j) % 3) == 0; -bool _check110(int i, int j) => (((i * j) % 2) + ((i * j) % 3)).isEven; -bool _check111(int i, int j) => (((i * j) % 3) + ((i + j) % 2)).isEven; +const int pattern000 = 0; +const int pattern001 = 1; +const int pattern010 = 2; +const int pattern011 = 3; +const int pattern100 = 4; +const int pattern101 = 5; +const int pattern110 = 6; +const int pattern111 = 7; diff --git a/qr/lib/src/math.dart b/qr/lib/src/math.dart index 6ff4c19..a5ab7bc 100644 --- a/qr/lib/src/math.dart +++ b/qr/lib/src/math.dart @@ -3,8 +3,7 @@ import 'dart:typed_data'; final Uint8List _logTable = _createLogTable(); final Uint8List _expTable = _createExpTable(); -int glog(int n) => - (n >= 1) ? _logTable[n] : throw ArgumentError.value(n, 'n', 'must be >= 1'); +int glog(int n) => (n >= 1) ? _logTable[n] : throw ArgumentError('glog($n)'); int gexp(int n) => _expTable[n % 255]; diff --git a/qr/lib/src/mode.dart b/qr/lib/src/mode.dart index c380f31..00efc50 100644 --- a/qr/lib/src/mode.dart +++ b/qr/lib/src/mode.dart @@ -1,55 +1,4 @@ -/// The encoding mode of a QR code segment. -enum QrMode { - /// Numeric mode (0-9). Most efficient. - numeric(1), - - /// Alphanumeric mode (0-9, A-Z, space, %, *, +, -, ., /, :). - alphaNumeric(2), - - /// Byte mode (8-bit data). - byte(4), - - /// Kanji mode (Shift-JIS). - kanji(8), - - /// Extended Channel Interpretation (ECI) mode. - eci(7); - - final int value; - - const QrMode(this.value); - - int getLengthBits(int type) { - if (this == eci) return 0; - if (type < 1 || type > 40) throw RangeError.range(type, 1, 40, 'type'); - - if (type < 10) { - // 1 - 9 - return switch (this) { - numeric => 10, - alphaNumeric => 9, - byte => 8, - kanji => 8, - eci => 0, - }; - } else if (type < 27) { - // 10 - 26 - return switch (this) { - numeric => 12, - alphaNumeric => 11, - byte => 16, - kanji => 10, - eci => 0, - }; - } else { - // 27 - 40 - return switch (this) { - numeric => 14, - alphaNumeric => 13, - byte => 16, - kanji => 12, - eci => 0, - }; - } - } -} +const int modeNumber = 1 << 0; +const int modeAlphaNum = 1 << 1; +const int mode8bitByte = 1 << 2; +const int modeKanji = 1 << 3; diff --git a/qr/lib/src/polynomial.dart b/qr/lib/src/polynomial.dart index 7a3641d..282ede9 100644 --- a/qr/lib/src/polynomial.dart +++ b/qr/lib/src/polynomial.dart @@ -28,75 +28,36 @@ class QrPolynomial { int get length => _values.length; QrPolynomial multiply(QrPolynomial e) { - final foo = Uint8List(length + e.length - 1); + final List foo = Uint8List(length + e.length - 1); for (var i = 0; i < length; i++) { - final v1 = _values[i]; - if (v1 == 0) continue; - final log1 = qr_math.glog(v1); for (var j = 0; j < e.length; j++) { - final v2 = e[j]; - if (v2 == 0) continue; - foo[i + j] ^= qr_math.gexp(log1 + qr_math.glog(v2)); + foo[i + j] ^= qr_math.gexp(qr_math.glog(this[i]) + qr_math.glog(e[j])); } } - return QrPolynomial._internal(foo); + return QrPolynomial(foo, 0); } QrPolynomial mod(QrPolynomial e) { if (length - e.length < 0) { + // ignore: avoid_returning_this return this; } - // Use a copy of _values that we will mutate - // We only need the part that will remain after modulo? - // Actually, standard polynomial division. - // We can work on a copy of `this._values` and zero out leading terms. + final ratio = qr_math.glog(this[0]) - qr_math.glog(e[0]); - final values = Uint8List.fromList(_values); + final value = Uint8List(length); - for (var i = 0; i < values.length - e.length + 1; i++) { - final v = values[i]; - if (v == 0) continue; - - final ratio = qr_math.glog(v) - qr_math.glog(e[0]); - - for (var j = 0; j < e.length; j++) { - final eVal = e[j]; - if (eVal == 0) continue; - values[i + j] ^= qr_math.gexp(qr_math.glog(eVal) + ratio); - } + for (var i = 0; i < length; i++) { + value[i] = this[i]; } - // The result is the remainder, which is the last e.length - 1 coefficients? - // Wait, the degree of remainder is less than degree of divisor (e). - // e.length is e.degree + 1. - // So remainder length is e.length - 1. + for (var i = 0; i < e.length; i++) { + value[i] ^= qr_math.gexp(qr_math.glog(e[i]) + ratio); + } - // Find where the remainder starts. - // In the loop above, we zeroed out terms from 0 to - // `values.length - e.length`. - // So the remainder starts at values.length - e.length + 1? - // No, we iterated i from 0 to diff. - // The loop eliminates the term at `i`. - // The last `i` is `values.length - e.length`. - // After that, the terms from `0` to `values.length - e.length` should be 0. - // The remainder is at the end. - - // Note: The original implementation used `offset` to skip leading zeros. - // `offset` increased when `values[offset] == 0`. - // My loop enforces `values[i]` becomes 0 (arithmetically, though likely not - // exactly 0 due to XOR, wait XOR equal things is 0). - - // Let's manually increment offset to match original logic if needed, - // or just slice the end. - // The remainder should fit in e.length - 1. - - // We can just return the tail. - // But we need to handle leading zeros in the result too? - // `QrPolynomial` constructor handles leading zeros. - - return QrPolynomial(values.sublist(values.length - e.length + 1), 0); + // recursive call + return QrPolynomial(value, 0).mod(e); } } diff --git a/qr/lib/src/qr_code.dart b/qr/lib/src/qr_code.dart index afc8a2c..3449a65 100644 --- a/qr/lib/src/qr_code.dart +++ b/qr/lib/src/qr_code.dart @@ -5,61 +5,66 @@ import 'package:meta/meta.dart'; import 'bit_buffer.dart'; import 'byte.dart'; -import 'eci.dart'; import 'error_correct_level.dart'; import 'input_too_long_exception.dart'; import 'math.dart' as qr_math; - +import 'mode.dart' as qr_mode; import 'polynomial.dart'; import 'rs_block.dart'; class QrCode { final int typeNumber; - final QrErrorCorrectLevel errorCorrectLevel; + final int errorCorrectLevel; final int moduleCount; List? _dataCache; final _dataList = []; QrCode(this.typeNumber, this.errorCorrectLevel) : moduleCount = typeNumber * 4 + 17 { - // The typeNumber is now calculated internally by the factories, - // so this check is only needed if QrCode is instantiated directly. - // However, the factories ensure a valid typeNumber is passed. - // Keeping it for direct instantiation safety. RangeError.checkValueInInterval(typeNumber, 1, 40, 'typeNumber'); + RangeError.checkValidIndex( + errorCorrectLevel, + QrErrorCorrectLevel.levels, + 'errorCorrectLevel', + ); } factory QrCode.fromData({ required String data, - required QrErrorCorrectLevel errorCorrectLevel, + required int errorCorrectLevel, }) { - final datumList = QrDatum.toDatums(data); - - final typeNumber = _calculateTypeNumberFromData( - errorCorrectLevel, - datumList, - ); - - final qrCode = QrCode(typeNumber, errorCorrectLevel); - for (final datum in datumList) { - qrCode._addToList(datum); + final QrDatum datum; + // Automatically determine mode here + if (QrNumeric.validationRegex.hasMatch(data)) { + // Numeric mode for numbers only + datum = QrNumeric.fromString(data); + } else if (QrAlphaNumeric.validationRegex.hasMatch(data)) { + // Alphanumeric mode for alphanumeric characters only + datum = QrAlphaNumeric.fromString(data); + } else { + // Default to byte mode for other characters + datum = QrByte(data); } + + final typeNumber = _calculateTypeNumberFromData(errorCorrectLevel, datum); + + final qrCode = QrCode(typeNumber, errorCorrectLevel).._addToList(datum); return qrCode; } factory QrCode.fromUint8List({ required Uint8List data, - required QrErrorCorrectLevel errorCorrectLevel, + required int errorCorrectLevel, }) { - final datum = QrByte.fromUint8List(data); - final typeNumber = _calculateTypeNumberFromData(errorCorrectLevel, [datum]); - return QrCode(typeNumber, errorCorrectLevel).._addToList(datum); + final typeNumber = _calculateTypeNumberFromData( + errorCorrectLevel, + QrByte.fromUint8List(data), + ); + return QrCode(typeNumber, errorCorrectLevel) + .._addToList(QrByte.fromUint8List(data)); } - static int _calculateTotalDataBits( - int typeNumber, - QrErrorCorrectLevel errorCorrectLevel, - ) { + static int _calculateTotalDataBits(int typeNumber, int errorCorrectLevel) { final rsBlocks = QrRsBlock.getRSBlocks(typeNumber, errorCorrectLevel); var totalDataBits = 0; for (var rsBlock in rsBlocks) { @@ -68,35 +73,26 @@ class QrCode { return totalDataBits; } - static int _calculateTypeNumberFromData( - QrErrorCorrectLevel errorCorrectLevel, - List data, - ) { + static int _calculateTypeNumberFromData(int errorCorrectLevel, QrDatum data) { for (var typeNumber = 1; typeNumber <= 40; typeNumber++) { final totalDataBits = _calculateTotalDataBits( typeNumber, errorCorrectLevel, ); - final buffer = QrBitBuffer(); - for (final datum in data) { - buffer - ..put(datum.mode.value, 4) - ..put(datum.length, datum.mode.getLengthBits(typeNumber)); - datum.write(buffer); - } + final buffer = QrBitBuffer() + ..put(data.mode, 4) + ..put(data.length, _lengthInBits(data.mode, typeNumber)); + data.write(buffer); if (buffer.length <= totalDataBits) return typeNumber; } // If we reach here, the data is too long for any QR Code version. - final buffer = QrBitBuffer(); - for (final datum in data) { - buffer - ..put(datum.mode.value, 4) - ..put(datum.length, datum.mode.getLengthBits(40)); - datum.write(buffer); - } + final buffer = QrBitBuffer() + ..put(data.mode, 4) + ..put(data.length, _lengthInBits(data.mode, 40)); + data.write(buffer); final maxBits = _calculateTotalDataBits(40, errorCorrectLevel); @@ -104,9 +100,19 @@ class QrCode { } void addData(String data) { - for (final datum in QrDatum.toDatums(data)) { - _addToList(datum); + final QrDatum datum; + // Automatically determine mode here, just like QrCode.fromData + if (QrNumeric.validationRegex.hasMatch(data)) { + // Numeric mode for numbers only + datum = QrNumeric.fromString(data); + } else if (QrAlphaNumeric.validationRegex.hasMatch(data)) { + // Alphanumeric mode for alphanumeric characters only + datum = QrAlphaNumeric.fromString(data); + } else { + // Default to byte mode for other characters + datum = QrByte(data); } + _addToList(datum); } void addByteData(ByteData data) => _addToList(QrByte.fromByteData(data)); @@ -121,8 +127,6 @@ class QrCode { void addAlphaNumeric(String alphaNumeric) => _addToList(QrAlphaNumeric.fromString(alphaNumeric)); - void addECI(int eciValue) => _addToList(QrEci(eciValue)); - void _addToList(QrDatum data) { _dataList.add(data); _dataCache = null; @@ -138,7 +142,7 @@ const int _pad1 = 0x11; List _createData( int typeNumber, - QrErrorCorrectLevel errorCorrectLevel, + int errorCorrectLevel, List dataList, ) { final rsBlocks = QrRsBlock.getRSBlocks(typeNumber, errorCorrectLevel); @@ -148,8 +152,8 @@ List _createData( for (var i = 0; i < dataList.length; i++) { final data = dataList[i]; buffer - ..put(data.mode.value, 4) - ..put(data.length, data.mode.getLengthBits(typeNumber)); + ..put(data.mode, 4) + ..put(data.length, _lengthInBits(data.mode, typeNumber)); data.write(buffer); } @@ -160,10 +164,6 @@ List _createData( errorCorrectLevel, ); - if (buffer.length > totalDataBits) { - throw InputTooLongException(buffer.length, totalDataBits); - } - // HUH? // èIí[ÉRÅ[Éh if (buffer.length + 4 <= totalDataBits) { @@ -244,6 +244,39 @@ List _createBytes(QrBitBuffer buffer, List rsBlocks) { return data; } +int _lengthInBits(int mode, int type) { + if (1 <= type && type < 10) { + // 1 - 9 + return switch (mode) { + qr_mode.modeNumber => 10, + qr_mode.modeAlphaNum => 9, + qr_mode.mode8bitByte => 8, + qr_mode.modeKanji => 8, + _ => throw ArgumentError('mode:$mode'), + }; + } else if (type < 27) { + // 10 - 26 + return switch (mode) { + qr_mode.modeNumber => 12, + qr_mode.modeAlphaNum => 11, + qr_mode.mode8bitByte => 16, + qr_mode.modeKanji => 10, + _ => throw ArgumentError('mode:$mode'), + }; + } else if (type < 41) { + // 27 - 40 + return switch (mode) { + qr_mode.modeNumber => 14, + qr_mode.modeAlphaNum => 13, + qr_mode.mode8bitByte => 16, + qr_mode.modeKanji => 12, + _ => throw ArgumentError('mode:$mode'), + }; + } else { + throw ArgumentError('type:$type'); + } +} + QrPolynomial _errorCorrectPolynomial(int errorCorrectLength) { var a = QrPolynomial([1], 0); diff --git a/qr/lib/src/qr_image.dart b/qr/lib/src/qr_image.dart index cbba6d1..beb09e2 100644 --- a/qr/lib/src/qr_image.dart +++ b/qr/lib/src/qr_image.dart @@ -1,72 +1,34 @@ -import 'dart:typed_data'; - import 'package:meta/meta.dart'; -import 'error_correct_level.dart'; -import 'mask_pattern.dart'; +import 'mask_pattern.dart' as qr_mask_pattern; import 'qr_code.dart'; import 'util.dart' as qr_util; /// Renders the encoded data from a [QrCode] in a portable format. class QrImage { - static const _pixelUnassigned = 0; - static const _pixelLight = 1; - static const _pixelDark = 2; - final int moduleCount; final int typeNumber; - final QrErrorCorrectLevel errorCorrectLevel; + final int errorCorrectLevel; final int maskPattern; - final Uint8List _data; + final _modules = >[]; /// Generates a QrImage with the best mask pattern encoding [qrCode]. factory QrImage(QrCode qrCode) { - // Create a template with invariant patterns - final template = QrImage._template(qrCode); - final moduleCount = template.moduleCount; - final dataSize = moduleCount * moduleCount; + var minLostPoint = 0.0; + QrImage? bestImage; - // Step 1: Clone template to working buffer and place data (no mask) - final dataMap = Uint8List(dataSize)..setRange(0, dataSize, template._data); - - // Create a temporary QrImage to use its _placeData method - // We pass 0 as maskPattern, but we will modify _placeData to NOT mask. - QrImage._fromData(qrCode, 0, dataMap)._placeData(qrCode.dataCache); - - final workingBuffer = Uint8List(dataSize); - var minLostPoint = double.maxFinite; - var bestMaskPattern = 0; - Uint8List? bestData; // We need to store the best result. - - // Step 2: Try all 8 masks for (var i = 0; i < 8; i++) { - // Copy pre-placed data to working buffer - workingBuffer.setRange(0, dataSize, dataMap); - - final testImage = QrImage._fromData(qrCode, i, workingBuffer) - // Apply mask (XOR) - .._applyMask(QrMaskPattern.values[i], template._data); - + final testImage = QrImage._test(qrCode, i); final lostPoint = _lostPoint(testImage); - if (lostPoint < minLostPoint) { + if (i == 0 || minLostPoint > lostPoint) { minLostPoint = lostPoint; - bestMaskPattern = i; - // Copy working buffer to bestData - bestData ??= Uint8List(dataSize); - bestData.setRange(0, dataSize, workingBuffer); + bestImage = testImage; } } - final finalImage = QrImage._fromData(qrCode, bestMaskPattern, bestData!) - // Final setup with correct format info (not test, so actual pixels) - .._setupTypeInfo(bestMaskPattern, false); - if (finalImage.typeNumber >= 7) { - finalImage._setupTypeNumber(false); - } - - return finalImage; + return QrImage.withMaskPattern(qrCode, bestImage!.maskPattern); } /// Generates a specific image for the [qrCode] and [maskPattern]. @@ -74,75 +36,35 @@ class QrImage { : assert(maskPattern >= 0 && maskPattern <= 7), moduleCount = qrCode.moduleCount, typeNumber = qrCode.typeNumber, - errorCorrectLevel = qrCode.errorCorrectLevel, - _data = Uint8List(qrCode.moduleCount * qrCode.moduleCount) { + errorCorrectLevel = qrCode.errorCorrectLevel { _makeImpl(maskPattern, qrCode.dataCache, false); } - /// Internal constructor for template creation - QrImage._template(QrCode qrCode) + QrImage._test(QrCode qrCode, this.maskPattern) : moduleCount = qrCode.moduleCount, typeNumber = qrCode.typeNumber, - errorCorrectLevel = qrCode.errorCorrectLevel, - maskPattern = 0, // Irrelevant - _data = Uint8List(qrCode.moduleCount * qrCode.moduleCount) { - // Setup invariant parts with test=true (reserving space) - _resetModules(); - _setupPositionProbePattern(0, 0); - _setupPositionProbePattern(moduleCount - 7, 0); - _setupPositionProbePattern(0, moduleCount - 7); - _setupPositionAdjustPattern(); - _setupTimingPattern(); - // Type info and Type number are invariant if test=true (all light) - _setupTypeInfo(0, true); - if (typeNumber >= 7) { - _setupTypeNumber(true); - } + errorCorrectLevel = qrCode.errorCorrectLevel { + _makeImpl(maskPattern, qrCode.dataCache, true); } - /// Internal constructor for testing phase - QrImage._fromData(QrCode qrCode, this.maskPattern, this._data) - : moduleCount = qrCode.moduleCount, - typeNumber = qrCode.typeNumber, - errorCorrectLevel = qrCode.errorCorrectLevel; - @visibleForTesting - List> get qrModules { - final list = >[]; - for (var r = 0; r < moduleCount; r++) { - final row = List.filled(moduleCount, null); - for (var c = 0; c < moduleCount; c++) { - final v = _data[r * moduleCount + c]; - row[c] = v == _pixelUnassigned ? null : (v == _pixelDark); - } - list.add(row); - } - return list; - } + List> get qrModules => _modules; void _resetModules() { - _data.fillRange(0, _data.length, _pixelUnassigned); + _modules.clear(); + for (var row = 0; row < moduleCount; row++) { + _modules.add(List.filled(moduleCount, null)); + } } bool isDark(int row, int col) { - if (row < 0 || moduleCount <= row) { - throw RangeError.range(row, 0, moduleCount - 1, 'row'); + if (row < 0 || moduleCount <= row || col < 0 || moduleCount <= col) { + throw ArgumentError('$row , $col'); } - if (col < 0 || moduleCount <= col) { - throw RangeError.range(col, 0, moduleCount - 1, 'col'); - } - return _data[row * moduleCount + col] == _pixelDark; - } - - void _set(int row, int col, bool value) { - _data[row * moduleCount + col] = value ? _pixelDark : _pixelLight; + return _modules[row][col]!; } void _makeImpl(int maskPattern, List dataCache, bool test) { - // If not testing, we do full setup. - // If testing (template), this method is NOT called directly, but manually - // in _template. - // However, withMaskPattern calls this. _resetModules(); _setupPositionProbePattern(0, 0); _setupPositionProbePattern(moduleCount - 7, 0); @@ -158,13 +80,6 @@ class QrImage { _mapData(dataCache, maskPattern); } - // ... (existing constructors) - - // Refactored _mapData to JUST call _placeData then _applyMask? - // No, original _mapData did both. - - // Implemented below... - void _setupPositionProbePattern(int row, int col) { for (var r = -1; r <= 7; r++) { if (row + r <= -1 || moduleCount <= row + r) continue; @@ -175,9 +90,9 @@ class QrImage { if ((0 <= r && r <= 6 && (c == 0 || c == 6)) || (0 <= c && c <= 6 && (r == 0 || r == 6)) || (2 <= r && r <= 4 && 2 <= c && c <= 4)) { - _set(row + r, col + c, true); + _modules[row + r][col + c] = true; } else { - _set(row + r, col + c, false); + _modules[row + r][col + c] = false; } } } @@ -191,16 +106,16 @@ class QrImage { final row = pos[i]; final col = pos[j]; - if (_data[row * moduleCount + col] != _pixelUnassigned) { + if (_modules[row][col] != null) { continue; } for (var r = -2; r <= 2; r++) { for (var c = -2; c <= 2; c++) { if (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) { - _set(row + r, col + c, true); + _modules[row + r][col + c] = true; } else { - _set(row + r, col + c, false); + _modules[row + r][col + c] = false; } } } @@ -210,22 +125,22 @@ class QrImage { void _setupTimingPattern() { for (var r = 8; r < moduleCount - 8; r++) { - if (_data[r * moduleCount + 6] != _pixelUnassigned) { + if (_modules[r][6] != null) { continue; } - _set(r, 6, r.isEven); + _modules[r][6] = r.isEven; } for (var c = 8; c < moduleCount - 8; c++) { - if (_data[6 * moduleCount + c] != _pixelUnassigned) { + if (_modules[6][c] != null) { continue; } - _set(6, c, c.isEven); + _modules[6][c] = c.isEven; } } void _setupTypeInfo(int maskPattern, bool test) { - final data = (errorCorrectLevel.index << 3) | maskPattern; + final data = (errorCorrectLevel << 3) | maskPattern; final bits = qr_util.bchTypeInfo(data); int i; @@ -236,11 +151,11 @@ class QrImage { mod = !test && ((bits >> i) & 1) == 1; if (i < 6) { - _set(i, 8, mod); + _modules[i][8] = mod; } else if (i < 8) { - _set(i + 1, 8, mod); + _modules[i + 1][8] = mod; } else { - _set(moduleCount - 15 + i, 8, mod); + _modules[moduleCount - 15 + i][8] = mod; } } @@ -249,16 +164,16 @@ class QrImage { mod = !test && ((bits >> i) & 1) == 1; if (i < 8) { - _set(8, moduleCount - i - 1, mod); + _modules[8][moduleCount - i - 1] = mod; } else if (i < 9) { - _set(8, 15 - i - 1 + 1, mod); + _modules[8][15 - i - 1 + 1] = mod; } else { - _set(8, 15 - i - 1, mod); + _modules[8][15 - i - 1] = mod; } } // fixed module - _set(moduleCount - 8, 8, !test); + _modules[moduleCount - 8][8] = !test; } void _setupTypeNumber(bool test) { @@ -266,12 +181,12 @@ class QrImage { for (var i = 0; i < 18; i++) { final mod = !test && ((bits >> i) & 1) == 1; - _set(i ~/ 3, i % 3 + moduleCount - 8 - 3, mod); + _modules[i ~/ 3][i % 3 + moduleCount - 8 - 3] = mod; } for (var i = 0; i < 18; i++) { final mod = !test && ((bits >> i) & 1) == 1; - _set(i % 3 + moduleCount - 8 - 3, i ~/ 3, mod); + _modules[i % 3 + moduleCount - 8 - 3][i ~/ 3] = mod; } } @@ -286,20 +201,20 @@ class QrImage { for (;;) { for (var c = 0; c < 2; c++) { - if (_data[row * moduleCount + (col - c)] == _pixelUnassigned) { + if (_modules[row][col - c] == null) { var dark = false; if (byteIndex < data.length) { dark = ((data[byteIndex] >> bitIndex) & 1) == 1; } - final mask = QrMaskPattern.values[maskPattern].check(row, col - c); + final mask = _mask(maskPattern, row, col - c); if (mask) { dark = !dark; } - _set(row, col - c, dark); + _modules[row][col - c] = dark; bitIndex--; if (bitIndex == -1) { @@ -319,123 +234,49 @@ class QrImage { } } } - - void _placeData(List data) { - var inc = -1; - var row = moduleCount - 1; - var bitIndex = 7; - var byteIndex = 0; - - for (var col = moduleCount - 1; col > 0; col -= 2) { - if (col == 6) col--; - - for (;;) { - for (var c = 0; c < 2; c++) { - if (_data[row * moduleCount + (col - c)] == _pixelUnassigned) { - var dark = false; - - if (byteIndex < data.length) { - dark = ((data[byteIndex] >> bitIndex) & 1) == 1; - } - - _set(row, col - c, dark); - bitIndex--; - - if (bitIndex == -1) { - byteIndex++; - bitIndex = 7; - } - } - } - - row += inc; - - if (row < 0 || moduleCount <= row) { - row -= inc; - inc = -inc; - break; - } - } - } - } - - void _applyMask(QrMaskPattern maskPattern, Uint8List templateData) { - var inc = -1; - var row = moduleCount - 1; - - for (var col = moduleCount - 1; col > 0; col -= 2) { - if (col == 6) col--; - - for (;;) { - for (var c = 0; c < 2; c++) { - if (templateData[row * moduleCount + (col - c)] == _pixelUnassigned) { - final mask = maskPattern.check(row, col - c); - if (mask) { - _data[row * moduleCount + (col - c)] ^= _pixelDark ^ _pixelLight; - } - } - } - - row += inc; - - if (row < 0 || moduleCount <= row) { - row -= inc; - inc = -inc; - break; - } - } - } - } } +bool _mask(int maskPattern, int i, int j) => switch (maskPattern) { + qr_mask_pattern.pattern000 => (i + j).isEven, + qr_mask_pattern.pattern001 => i.isEven, + qr_mask_pattern.pattern010 => j % 3 == 0, + qr_mask_pattern.pattern011 => (i + j) % 3 == 0, + qr_mask_pattern.pattern100 => ((i ~/ 2) + (j ~/ 3)).isEven, + qr_mask_pattern.pattern101 => (i * j) % 2 + (i * j) % 3 == 0, + qr_mask_pattern.pattern110 => ((i * j) % 2 + (i * j) % 3).isEven, + qr_mask_pattern.pattern111 => ((i * j) % 3 + (i + j) % 2).isEven, + _ => throw ArgumentError('bad maskPattern:$maskPattern'), +}; + double _lostPoint(QrImage qrImage) { final moduleCount = qrImage.moduleCount; - final data = qrImage._data; + var lostPoint = 0.0; + int row, col; - // Cache data length for faster access (though it's final) - // Accessing local vars is faster. - - // Level 1 - for (var row = 0; row < moduleCount; row++) { - for (var col = 0; col < moduleCount; col++) { + // LEVEL1 + for (row = 0; row < moduleCount; row++) { + for (col = 0; col < moduleCount; col++) { var sameCount = 0; - final currentIdx = row * moduleCount + col; - final isDark = data[currentIdx] == QrImage._pixelDark; + final dark = qrImage.isDark(row, col); - // Check all 8 neighbors - // Top row - if (row > 0) { - final upIdx = currentIdx - moduleCount; - if (col > 0 && (data[upIdx - 1] == QrImage._pixelDark) == isDark) { - sameCount++; + for (var r = -1; r <= 1; r++) { + if (row + r < 0 || moduleCount <= row + r) { + continue; } - if ((data[upIdx] == QrImage._pixelDark) == isDark) sameCount++; - if (col < moduleCount - 1 && - (data[upIdx + 1] == QrImage._pixelDark) == isDark) { - sameCount++; - } - } - // Middle row (left/right) - if (col > 0 && (data[currentIdx - 1] == QrImage._pixelDark) == isDark) { - sameCount++; - } - if (col < moduleCount - 1 && - (data[currentIdx + 1] == QrImage._pixelDark) == isDark) { - sameCount++; - } + for (var c = -1; c <= 1; c++) { + if (col + c < 0 || moduleCount <= col + c) { + continue; + } - // Bottom row - if (row < moduleCount - 1) { - final downIdx = currentIdx + moduleCount; - if (col > 0 && (data[downIdx - 1] == QrImage._pixelDark) == isDark) { - sameCount++; - } - if ((data[downIdx] == QrImage._pixelDark) == isDark) sameCount++; - if (col < moduleCount - 1 && - (data[downIdx + 1] == QrImage._pixelDark) == isDark) { - sameCount++; + if (r == 0 && c == 0) { + continue; + } + + if (dark == qrImage.isDark(row + r, col + c)) { + sameCount++; + } } } @@ -445,58 +286,58 @@ double _lostPoint(QrImage qrImage) { } } - // Level 2: 2x2 blocks of same color - for (var row = 0; row < moduleCount - 1; row++) { - for (var col = 0; col < moduleCount - 1; col++) { - final idx = row * moduleCount + col; - final p00 = data[idx]; - final p01 = data[idx + 1]; - final p10 = data[idx + moduleCount]; - final p11 = data[idx + moduleCount + 1]; - - if (p00 == p01 && p00 == p10 && p00 == p11) { + // LEVEL2 + for (row = 0; row < moduleCount - 1; row++) { + for (col = 0; col < moduleCount - 1; col++) { + var count = 0; + if (qrImage.isDark(row, col)) count++; + if (qrImage.isDark(row + 1, col)) count++; + if (qrImage.isDark(row, col + 1)) count++; + if (qrImage.isDark(row + 1, col + 1)) count++; + if (count == 0 || count == 4) { lostPoint += 3; } } } - // Level 3: 1:1:3:1:1 pattern - // Dark, Light, Dark, Dark, Dark, Light, Dark - for (var row = 0; row < moduleCount; row++) { - for (var col = 0; col < moduleCount - 6; col++) { - final idx = row * moduleCount + col; - if (data[idx] == QrImage._pixelDark && - data[idx + 1] == QrImage._pixelLight && - data[idx + 2] == QrImage._pixelDark && - data[idx + 3] == QrImage._pixelDark && - data[idx + 4] == QrImage._pixelDark && - data[idx + 5] == QrImage._pixelLight && - data[idx + 6] == QrImage._pixelDark) { + // LEVEL3 + for (row = 0; row < moduleCount; row++) { + for (col = 0; col < moduleCount - 6; col++) { + if (qrImage.isDark(row, col) && + !qrImage.isDark(row, col + 1) && + qrImage.isDark(row, col + 2) && + qrImage.isDark(row, col + 3) && + qrImage.isDark(row, col + 4) && + !qrImage.isDark(row, col + 5) && + qrImage.isDark(row, col + 6)) { lostPoint += 40; } } } - // Check cols - for (var col = 0; col < moduleCount; col++) { - for (var row = 0; row < moduleCount - 6; row++) { - final idx = row * moduleCount + col; - if (data[idx] == QrImage._pixelDark && - data[idx + moduleCount] == QrImage._pixelLight && - data[idx + 2 * moduleCount] == QrImage._pixelDark && - data[idx + 3 * moduleCount] == QrImage._pixelDark && - data[idx + 4 * moduleCount] == QrImage._pixelDark && - data[idx + 5 * moduleCount] == QrImage._pixelLight && - data[idx + 6 * moduleCount] == QrImage._pixelDark) { + for (col = 0; col < moduleCount; col++) { + for (row = 0; row < moduleCount - 6; row++) { + if (qrImage.isDark(row, col) && + !qrImage.isDark(row + 1, col) && + qrImage.isDark(row + 2, col) && + qrImage.isDark(row + 3, col) && + qrImage.isDark(row + 4, col) && + !qrImage.isDark(row + 5, col) && + qrImage.isDark(row + 6, col)) { lostPoint += 40; } } } - // Level 4: Dark ratio + // LEVEL4 var darkCount = 0; - for (var i = 0; i < data.length; i++) { - if (data[i] == QrImage._pixelDark) darkCount++; + + for (col = 0; col < moduleCount; col++) { + for (row = 0; row < moduleCount; row++) { + if (qrImage.isDark(row, col)) { + darkCount++; + } + } } final ratio = (100 * darkCount / moduleCount / moduleCount - 50).abs() / 5; diff --git a/qr/lib/src/rs_block.dart b/qr/lib/src/rs_block.dart index 62b3e0a..206a016 100644 --- a/qr/lib/src/rs_block.dart +++ b/qr/lib/src/rs_block.dart @@ -6,10 +6,7 @@ class QrRsBlock { QrRsBlock._(this.totalCount, this.dataCount); - static List getRSBlocks( - int typeNumber, - QrErrorCorrectLevel errorCorrectLevel, - ) { + static List getRSBlocks(int typeNumber, int errorCorrectLevel) { final rsBlock = _getRsBlockTable(typeNumber, errorCorrectLevel); final length = rsBlock.length ~/ 3; @@ -32,12 +29,15 @@ class QrRsBlock { List _getRsBlockTable( int typeNumber, - QrErrorCorrectLevel errorCorrectLevel, + int errorCorrectLevel, ) => switch (errorCorrectLevel) { - QrErrorCorrectLevel.low => _rsBlockTable[(typeNumber - 1) * 4 + 0], - QrErrorCorrectLevel.medium => _rsBlockTable[(typeNumber - 1) * 4 + 1], - QrErrorCorrectLevel.quartile => _rsBlockTable[(typeNumber - 1) * 4 + 2], - QrErrorCorrectLevel.high => _rsBlockTable[(typeNumber - 1) * 4 + 3], + QrErrorCorrectLevel.L => _rsBlockTable[(typeNumber - 1) * 4 + 0], + QrErrorCorrectLevel.M => _rsBlockTable[(typeNumber - 1) * 4 + 1], + QrErrorCorrectLevel.Q => _rsBlockTable[(typeNumber - 1) * 4 + 2], + QrErrorCorrectLevel.H => _rsBlockTable[(typeNumber - 1) * 4 + 3], + _ => throw ArgumentError( + 'bad rs block @ typeNumber: $typeNumber/errorCorrectLevel:$errorCorrectLevel', + ), }; const List> _rsBlockTable = [ diff --git a/qr/pubspec.yaml b/qr/pubspec.yaml index 6ba3673..48a112e 100644 --- a/qr/pubspec.yaml +++ b/qr/pubspec.yaml @@ -12,13 +12,9 @@ dependencies: meta: ^1.7.0 dev_dependencies: - args: ^2.1.0 - benchmark_harness: ^2.0.0 build_runner: ^2.2.1 build_web_compilers: ^4.1.4 dart_flutter_team_lints: ^3.0.0 - path: ^1.9.1 stream_transform: ^2.0.0 test: ^1.21.6 - test_process: ^2.1.1 web: ^1.1.0 diff --git a/qr/test/eci_test.dart b/qr/test/eci_test.dart deleted file mode 100644 index ef14925..0000000 --- a/qr/test/eci_test.dart +++ /dev/null @@ -1,564 +0,0 @@ -// ignore_for_file: lines_longer_than_80_chars - -import 'package:qr/qr.dart'; -import 'package:qr/src/mode.dart' as qr_mode; -import 'package:test/test.dart'; - -void main() { - group('QrEci', () { - test('validates value range', () { - expect(() => QrEci(-1), throwsArgumentError); - expect(() => QrEci(1000000), throwsArgumentError); - expect(QrEci(0).value, 0); - expect(QrEci(999999).value, 999999); - }); - - test('constants', () { - expect(QrEciValue.iso8859_1, 3); - expect(QrEciValue.iso8859_2, 4); - expect(QrEciValue.iso8859_3, 5); - expect(QrEciValue.iso8859_4, 6); - expect(QrEciValue.iso8859_5, 7); - expect(QrEciValue.iso8859_6, 8); - expect(QrEciValue.iso8859_7, 9); - expect(QrEciValue.iso8859_8, 10); - expect(QrEciValue.iso8859_9, 11); - expect(QrEciValue.iso8859_10, 12); - expect(QrEciValue.iso8859_11, 13); - expect(QrEciValue.iso8859_13, 15); - expect(QrEciValue.iso8859_14, 16); - expect(QrEciValue.iso8859_15, 17); - expect(QrEciValue.iso8859_16, 18); - expect(QrEciValue.shiftJis, 20); - expect(QrEciValue.windows1250, 21); - expect(QrEciValue.windows1251, 22); - expect(QrEciValue.windows1252, 23); - expect(QrEciValue.windows1256, 24); - expect(QrEciValue.utf16BE, 25); - expect(QrEciValue.utf8, 26); - expect(QrEciValue.ascii, 27); - expect(QrEciValue.big5, 28); - expect(QrEciValue.gb2312, 29); - expect(QrEciValue.eucKr, 30); - expect(QrEciValue.gbk, 31); - }); - - test('properties', () { - final eci = QrEci(123); - expect(eci.mode, qr_mode.QrMode.eci); - expect(eci.length, 0); - }); - - test('encodes 0-127 (8 bits)', () { - _testEci(0, [0x00]); // 00000000 - _testEci(65, [0x41]); // 01000001 - _testEci(127, [0x7F]); // 01111111 - }); - - test('encodes 128-16383 (16 bits)', () { - // 128 -> 10 000000 10000000 -> 0x80 0x80 - _testEci(128, [0x80, 0x80]); - // 16383 -> 10 111111 11111111 -> 0xBF 0xFF - _testEci(16383, [0xBF, 0xFF]); - }); - - test('encodes 16384-999999 (24 bits)', () { - // 16384 -> 110 00000 01000000 00000000 -> 0xC0 0x40 0x00 - _testEci(16384, [0xC0, 0x40, 0x00]); - // 999999 -> 11110100001000111111 -> 0F 42 3F - // 999999 = 0xF423F - // 110 01111 01000010 00111111 -> 0xCF 0x42 0x3F - _testEci(999999, [0xCF, 0x42, 0x3F]); - }); - }); - - test('validates emoji', () { - final code = QrCode(1, QrErrorCorrectLevel.low)..addData('🙃'); - - // Validate bitstream structure: - // Header: Mode 7 (0111) + Value 26 (00011010) + Mode 4 (0100) + Length 4 (00000100) - // 0111 0001 1010 0100 0000 0100 -> 0x71 0xA4 0x04 - // Data: F0 9F 99 83 - // Terminator: 0000 - // Padding to byte: 0000 (since 60 bits + 4 bits = 64 bits = 8 bytes) - // Pad Bytes: 0xEC, 0x11... (to fill 19 bytes) - final expectedData = [ - 0x71, 0xA4, 0x04, // Header - 0xF0, 0x9F, 0x99, 0x83, // '🙃' in UTF-8 - 0x00, // Terminator + Bit Padding to byte boundary - // Padding Codewords (0xEC, 0x11 alternating) to fill 19 bytes capacity - 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, 0x11, 0xEC, - ]; - - // Verify the full data cache (19 Data Codewords for Version 1-L) - expect(code.dataCache.sublist(0, 19), expectedData); - - final image = QrImage(code); - expect(image.moduleCount, 21); // Version 1 is 21x21 - expect(_getModules(image), _expectedEmojiModules); - }); -} - -void _testEci(int value, List expectedBytes) { - final buffer = QrBitBuffer(); - QrEci(value).write(buffer); - - expect(buffer, hasLength(expectedBytes.length * 8)); - for (var i = 0; i < expectedBytes.length; i++) { - expect(buffer.getByte(i), expectedBytes[i], reason: 'Byte $i mismatch'); - } -} - -List _getModules(QrImage image) { - final modules = []; - for (var i = 0; i < image.moduleCount; i++) { - for (var j = 0; j < image.moduleCount; j++) { - modules.add(image.isDark(i, j)); - } - } - return modules; -} - -const _expectedEmojiModules = [ - true, - true, - true, - true, - true, - true, - true, - false, - false, - true, - false, - false, - true, - false, - true, - true, - true, - true, - true, - true, - true, // Row 0 - true, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - true, - false, - false, - true, - false, - false, - false, - false, - false, - true, // Row 1 - true, - false, - true, - true, - true, - false, - true, - false, - false, - true, - false, - false, - false, - false, - true, - false, - true, - true, - true, - false, - true, // Row 2 - true, - false, - true, - true, - true, - false, - true, - false, - true, - false, - false, - true, - false, - false, - true, - false, - true, - true, - true, - false, - true, // Row 3 - true, - false, - true, - true, - true, - false, - true, - false, - false, - false, - true, - true, - true, - false, - true, - false, - true, - true, - true, - false, - true, // Row 4 - true, - false, - false, - false, - false, - false, - true, - false, - true, - true, - true, - false, - true, - false, - true, - false, - false, - false, - false, - false, - true, // Row 5 - true, - true, - true, - true, - true, - true, - true, - false, - true, - false, - true, - false, - true, - false, - true, - true, - true, - true, - true, - true, - true, // Row 6 - false, - false, - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - false, - false, - false, - false, - false, - false, - false, - false, // Row 7 - true, - true, - true, - true, - true, - false, - true, - true, - true, - true, - false, - false, - true, - true, - false, - true, - false, - true, - false, - true, - false, // Row 8 - false, - true, - true, - true, - false, - false, - false, - true, - true, - false, - false, - false, - true, - false, - false, - true, - false, - true, - false, - false, - false, // Row 9 - true, - true, - false, - false, - false, - true, - true, - false, - false, - true, - true, - true, - false, - true, - false, - false, - true, - true, - false, - true, - false, // Row 10 - true, - false, - true, - true, - true, - false, - false, - true, - true, - false, - true, - false, - false, - false, - false, - true, - true, - false, - true, - false, - false, // Row 11 - false, - true, - false, - false, - true, - true, - true, - true, - false, - true, - false, - true, - false, - true, - false, - false, - true, - false, - true, - false, - false, // Row 12 - false, - false, - false, - false, - false, - false, - false, - false, - true, - true, - true, - true, - true, - true, - true, - true, - false, - false, - false, - false, - false, // Row 13 - true, - true, - true, - true, - true, - true, - true, - false, - true, - true, - false, - false, - true, - false, - true, - true, - false, - true, - true, - true, - false, // Row 14 - true, - false, - false, - false, - false, - false, - true, - false, - false, - true, - false, - true, - true, - true, - true, - true, - false, - true, - false, - false, - true, // Row 15 - true, - false, - true, - true, - true, - false, - true, - false, - true, - false, - false, - false, - true, - false, - false, - true, - false, - true, - false, - false, - true, // Row 16 - true, - false, - true, - true, - true, - false, - true, - false, - true, - false, - false, - false, - true, - false, - false, - false, - true, - false, - false, - true, - false, // Row 17 - true, - false, - true, - true, - true, - false, - true, - false, - true, - false, - true, - true, - false, - true, - false, - true, - false, - true, - true, - false, - false, // Row 18 - true, - false, - false, - false, - false, - false, - true, - false, - true, - false, - false, - false, - false, - false, - false, - true, - false, - false, - false, - true, - true, // Row 19 - true, - true, - true, - true, - true, - true, - true, - false, - true, - true, - true, - true, - false, - true, - false, - true, - true, - true, - true, - true, - false, // Row 20 -]; diff --git a/qr/test/qr_alphanumeric_test.dart b/qr/test/qr_alphanumeric_test.dart index 9af0781..68da9de 100644 --- a/qr/test/qr_alphanumeric_test.dart +++ b/qr/test/qr_alphanumeric_test.dart @@ -1,4 +1,5 @@ import 'package:qr/qr.dart'; +import 'package:qr/src/byte.dart'; import 'package:test/test.dart'; void main() { @@ -6,11 +7,11 @@ void main() { final qr = QrAlphaNumeric.fromString( r'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:', ); - expect(qr.mode, QrMode.alphaNumeric); + expect(qr.mode, 2); expect(qr.length, 45); final buffer = QrBitBuffer(); qr.write(buffer); - expect(buffer, hasLength(248)); + expect(buffer.length, 248); expect( buffer.map((e) => e ? '1' : '0').join(), '00000000001' @@ -41,21 +42,21 @@ void main() { test('single alphanumeric', () { final qr = QrAlphaNumeric.fromString(r'$'); - expect(qr.mode, QrMode.alphaNumeric); + expect(qr.mode, 2); expect(qr.length, 1); final buffer = QrBitBuffer(); qr.write(buffer); - expect(buffer, hasLength(6)); + expect(buffer.length, 6); expect(buffer.map((e) => e ? '1' : '0').join(), '100101'); }); test('double (even) alphanumeric', () { final qr = QrAlphaNumeric.fromString('3Z'); - expect(qr.mode, QrMode.alphaNumeric); + expect(qr.mode, 2); expect(qr.length, 2); final buffer = QrBitBuffer(); qr.write(buffer); - expect(buffer, hasLength(11), reason: 'n*5+1 = 11'); + expect(buffer.length, 11, reason: 'n*5+1 = 11'); expect( buffer .getRange(0, 11) diff --git a/qr/test/qr_byte_test.dart b/qr/test/qr_byte_test.dart index 6b18f8f..9c922cd 100644 --- a/qr/test/qr_byte_test.dart +++ b/qr/test/qr_byte_test.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:qr/qr.dart'; +import 'package:qr/src/byte.dart'; import 'package:test/test.dart'; void main() { diff --git a/qr/test/qr_code_test.dart b/qr/test/qr_code_test.dart index 6186e61..b75b7e0 100644 --- a/qr/test/qr_code_test.dart +++ b/qr/test/qr_code_test.dart @@ -11,32 +11,36 @@ import 'qr_code_test_data_with_mask.dart'; void main() { test('simple', () { for (var typeNumber = 1; typeNumber <= 40; typeNumber++) { - for (var quality in QrErrorCorrectLevel.values) { + for (var quality in QrErrorCorrectLevel.levels) { final qr = QrImage(QrCode(typeNumber, quality)..addData('shanna!')); final modules = qr.qrModules; - expect( - modules.map(_encodeBoolListToString), - qrCodeTestData[typeNumber.toString()][quality.index.toString()], - ); + for (var i = 0; i < modules.length; i++) { + expect( + _encodeBoolListToString(modules[i]), + qrCodeTestData[typeNumber.toString()][quality.toString()][i], + ); + } } } }); test('fromData', () { - for (var quality in QrErrorCorrectLevel.values) { + for (var quality in QrErrorCorrectLevel.levels) { final qr = QrImage( QrCode.fromData(data: 'shanna!', errorCorrectLevel: quality), ); final modules = qr.qrModules; - expect( - modules.map(_encodeBoolListToString), - qrCodeTestData['1'][quality.index.toString()], - ); + for (var i = 0; i < modules.length; i++) { + expect( + _encodeBoolListToString(modules[i]), + qrCodeTestData['1'][quality.toString()][i], + ); + } } }); test('fromUint8List', () { - for (var quality in QrErrorCorrectLevel.values) { + for (var quality in QrErrorCorrectLevel.levels) { final qr = QrImage( QrCode.fromUint8List( data: Uint8List.fromList([115, 104, 97, 110, 110, 97, 33]), @@ -44,53 +48,67 @@ void main() { ), ); final modules = qr.qrModules; - expect( - modules.map(_encodeBoolListToString), - qrCodeTestData['1'][quality.index.toString()], - ); + for (var i = 0; i < modules.length; i++) { + expect( + _encodeBoolListToString(modules[i]), + qrCodeTestData['1'][quality.toString()][i], + ); + } } }); test('WHEN mask pattern is provided, SHOULD make a masked QR Code', () { for (var mask = 0; mask <= 7; mask++) { final qr = QrImage.withMaskPattern( - QrCode(1, QrErrorCorrectLevel.low)..addData('shanna!'), + QrCode(1, QrErrorCorrectLevel.L)..addData('shanna!'), mask, ); final modules = qr.qrModules; - expect( - modules.map(_encodeBoolListToString), - qrCodeTestDataWithMask[mask.toString()], - ); + for (var i = 0; i < modules.length; i++) { + expect( + _encodeBoolListToString(modules[i]), + qrCodeTestDataWithMask[mask.toString()][i], + ); + } } }); - test('WHEN provided mask pattern is smaller than 0, ' - 'SHOULD throw an AssertionError', () { - expect(() { - QrImage.withMaskPattern( - QrCode(1, QrErrorCorrectLevel.low)..addData('shanna!'), - -1, - ); - }, throwsA(isA())); - }); + test( + ''' + WHEN provided mask pattern is smaller than 0, + SHOULD throw an AssertionError + ''', + () { + expect(() { + QrImage.withMaskPattern( + QrCode(1, QrErrorCorrectLevel.L)..addData('shanna!'), + -1, + ); + }, throwsA(isA())); + }, + ); - test('WHEN provided mask pattern is bigger than 7, ' - 'SHOULD throw an AssertionError', () { - expect(() { - QrImage.withMaskPattern( - QrCode(1, QrErrorCorrectLevel.high)..addData('shanna!'), - 8, - ); - }, throwsA(isA())); - }); + test( + ''' + WHEN provided mask pattern is bigger than 7, + SHOULD throw an AssertionError + ''', + () { + expect(() { + QrImage.withMaskPattern( + QrCode(1, QrErrorCorrectLevel.L)..addData('shanna!'), + 8, + ); + }, throwsA(isA())); + }, + ); group('QrCode.fromData Automatic Mode Detection', () { // Numeric Mode test('should use Numeric Mode for numbers', () { // 9 numeric chars fit version 1 (H level). final qr = QrCode.fromData( data: '123456789', - errorCorrectLevel: QrErrorCorrectLevel.high, + errorCorrectLevel: QrErrorCorrectLevel.H, ); expect(qr.typeNumber, 1); }); @@ -101,7 +119,7 @@ void main() { // version 2 (H level, 16 chars). final qr = QrCode.fromData( data: 'HELLO WORLD A', - errorCorrectLevel: QrErrorCorrectLevel.high, + errorCorrectLevel: QrErrorCorrectLevel.H, ); expect(qr.typeNumber, 2); }); @@ -112,7 +130,7 @@ void main() { // '機械学習' (12 bytes) fits version 2 (H level, 16 bytes). final qr = QrCode.fromData( data: '機械学習', - errorCorrectLevel: QrErrorCorrectLevel.high, + errorCorrectLevel: QrErrorCorrectLevel.H, ); expect(qr.typeNumber, 2); }); @@ -122,7 +140,7 @@ void main() { // Numeric Mode test('should use Numeric Mode for numbers', () { // 9 numeric characters fit version 1 (H level). - final qr = QrCode(1, QrErrorCorrectLevel.low)..addData('123456789'); + final qr = QrCode(1, QrErrorCorrectLevel.H)..addData('123456789'); expect(qr.typeNumber, 1); }); @@ -130,7 +148,7 @@ void main() { test('should use Alphanumeric Mode', () { // 13 alphanumeric characters exceed version 1 (7 chars) but fit // version 2 (H level, 16 chars). - final qr = QrCode(2, QrErrorCorrectLevel.high)..addData('HELLO WORLD A'); + final qr = QrCode(2, QrErrorCorrectLevel.H)..addData('HELLO WORLD A'); expect(qr.typeNumber, 2); }); @@ -138,7 +156,7 @@ void main() { test('should use Byte Mode for non-alphanumeric characters', () { // Kanji characters are UTF-8 encoded. // '機械学習' (12 bytes) fits version 2 (H level, 16 bytes). - final qr = QrCode(2, QrErrorCorrectLevel.high)..addData('機械学習'); + final qr = QrCode(2, QrErrorCorrectLevel.H)..addData('機械学習'); expect(qr.typeNumber, 2); }); }); @@ -150,7 +168,7 @@ void main() { final qrCode = QrCode.fromData( data: largeData, - errorCorrectLevel: QrErrorCorrectLevel.low, + errorCorrectLevel: QrErrorCorrectLevel.L, ); expect(qrCode.typeNumber, 40); @@ -164,20 +182,12 @@ void main() { expect( () => QrCode.fromData( data: excessivelyLargeData, - errorCorrectLevel: QrErrorCorrectLevel.low, + errorCorrectLevel: QrErrorCorrectLevel.L, ), throwsA(isA()), ); }); }); - - group('QrCode.addData size checks', () { - test('should throw if data exceeds capacity for fixed version', () { - final code = QrCode(1, QrErrorCorrectLevel.low)..addData('|' * 30); - - expect(() => code.dataCache, throwsA(isA())); - }); - }); } String _encodeBoolListToString(List source) => diff --git a/qr/test/qr_datum_test.dart b/qr/test/qr_datum_test.dart deleted file mode 100644 index 2a55ffb..0000000 --- a/qr/test/qr_datum_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:qr/src/byte.dart'; -import 'package:qr/src/eci.dart'; -import 'package:test/test.dart'; - -void main() { - group('QrDatum.toDatums', () { - test('Numeric', () { - final datums = QrDatum.toDatums('123456'); - expect(datums, hasLength(1)); - expect(datums.first, isA()); - }); - - test('AlphaNumeric', () { - final datums = QrDatum.toDatums('HELLO WORLD'); - expect(datums, hasLength(1)); - expect(datums.first, isA()); - }); - - test('Byte (Latin-1)', () { - final datums = QrDatum.toDatums('Hello World!'); - expect(datums, hasLength(1)); - expect(datums.first, isA()); - }); - - test('Byte (UTF-8 with ECI)', () { - final datums = QrDatum.toDatums('Hello 🌍'); - expect(datums, hasLength(2)); - expect(datums[0], isA()); - expect((datums[0] as QrEci).value, 26); - expect(datums[1], isA()); - }); - - test('Complex Emoji (UTF-8 with ECI)', () { - // Woman + Medium Skin Tone + ZWJ + Heart + VS16 + ZWJ + Kiss Mark + ZWJ - // + Man + Dark Brown Skin Tone - const complexEmoji = - '\u{1F469}\u{1F3FD}\u{200D}\u{2764}\u{FE0F}\u{200D}' - '\u{1F48B}\u{200D}\u{1F468}\u{1F3FE}'; - final datums = QrDatum.toDatums(complexEmoji); - expect(datums, hasLength(2)); - expect(datums[0], isA()); - expect((datums[0] as QrEci).value, 26); - expect(datums[1], isA()); - }); - }); -} diff --git a/qr/test/qr_numeric_test.dart b/qr/test/qr_numeric_test.dart index 7ad72c9..2d53439 100644 --- a/qr/test/qr_numeric_test.dart +++ b/qr/test/qr_numeric_test.dart @@ -1,14 +1,15 @@ import 'package:qr/qr.dart'; +import 'package:qr/src/byte.dart'; import 'package:test/test.dart'; void main() { test('all digits 1 through 0', () { final qr = QrNumeric.fromString('1234567890'); - expect(qr.mode, QrMode.numeric); + expect(qr.mode, 1); expect(qr.length, 10); final buffer = QrBitBuffer(); qr.write(buffer); - expect(buffer, hasLength(34)); + expect(buffer.length, 34); expect( buffer .getRange(0, 10) @@ -53,11 +54,11 @@ void main() { test('single numeric', () { final qr = QrNumeric.fromString('5'); - expect(qr.mode, QrMode.numeric); + expect(qr.mode, 1); expect(qr.length, 1); final buffer = QrBitBuffer(); qr.write(buffer); - expect(buffer, hasLength(4)); + expect(buffer.length, 4); expect( buffer .getRange(0, 4) @@ -72,11 +73,11 @@ void main() { test('double numeric', () { final qr = QrNumeric.fromString('37'); - expect(qr.mode, QrMode.numeric); + expect(qr.mode, 1); expect(qr.length, 2); final buffer = QrBitBuffer(); qr.write(buffer); - expect(buffer, hasLength(7), reason: 'n*3+1 = 7'); + expect(buffer.length, 7, reason: 'n*3+1 = 7'); expect( buffer .getRange(0, 7) @@ -91,11 +92,11 @@ void main() { test('triple (even) numeric', () { final qr = QrNumeric.fromString('371'); - expect(qr.mode, QrMode.numeric); + expect(qr.mode, 1); expect(qr.length, 3); final buffer = QrBitBuffer(); qr.write(buffer); - expect(buffer, hasLength(10), reason: 'n*3+1 = 10'); + expect(buffer.length, 10, reason: 'n*3+1 = 10'); expect( buffer .getRange(0, 10) diff --git a/qr/test/verify_emoji_test.dart b/qr/test/verify_emoji_test.dart deleted file mode 100644 index 2ca2db0..0000000 --- a/qr/test/verify_emoji_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:qr/qr.dart'; -import 'package:test/test.dart'; - -void main() { - test('Generate QR with Emoji', () { - const emojiString = '👩🏽❤️💋👨🏾'; - final qr = QrCode.fromData( - data: emojiString, - errorCorrectLevel: QrErrorCorrectLevel.low, - ); - expect(qr.typeNumber, 2); - expect(qr.typeNumber, greaterThan(0)); - // Verify we have multiple segments (ECI + Byte) - // iterate over modules or check internal structure if possible - // (but it's private) - }); - - test('Generate QR with Complex Emoji (ZWJ support)', () { - // Woman + Medium Skin Tone + ZWJ + Heart + VS16 + ZWJ + Kiss Mark + ZWJ - // + Man + Dark Brown Skin Tone - const complexEmoji = - '\u{1F469}\u{1F3FD}\u{200D}\u{2764}\u{FE0F}\u{200D}' - '\u{1F48B}\u{200D}\u{1F468}\u{1F3FE}'; - - final qr = QrCode.fromData( - data: complexEmoji, - errorCorrectLevel: QrErrorCorrectLevel.low, - ); - expect(qr.typeNumber, greaterThan(0)); - // Verify it didn't throw and created a valid QR structure - // The exact type number depends on the overhead of ECI + Byte mode - - // 4 segments: - // 1. ECI (26 for UTF-8) - // 2. Byte Data (the emoji bytes) - - // We can't easily peek into _dataList, but we can verify the module count - // implies it's not empty - expect(qr.moduleCount, greaterThan(21)); - }); -} diff --git a/qr/test/verify_qr_tool_test.dart b/qr/test/verify_qr_tool_test.dart deleted file mode 100644 index f0dc120..0000000 --- a/qr/test/verify_qr_tool_test.dart +++ /dev/null @@ -1,173 +0,0 @@ -@Tags(['require-zbar']) -library; - -import 'dart:io'; - -import 'package:path/path.dart' as p; -import 'package:test/test.dart'; -import 'package:test_process/test_process.dart'; - -void main() { - late Directory tempDir; - - setUpAll(() { - tempDir = Directory.systemTemp.createTempSync('qr_tool_test'); - }); - - tearDownAll(() { - tempDir.deleteSync(recursive: true); - }); - - final configurations = [ - (version: null, correction: null), - (version: 40, correction: 'H'), - ]; - - final inputs = [ - '123456', - 'HELLO WORLD', - 'Hello 👋 World 🌍', - '👩🏽❤️💋👨🏾', - '👩🏽‍❤️‍💋‍👨🏾', - ]; - - for (final config in configurations) { - for (final input in inputs) { - test( - 'Generate QR with config $config and input "$input"', - () async { - final bmpPath = p.join( - tempDir.path, - 'test_${config.hashCode}_${input.hashCode}.bmp', - ); - final args = [ - 'tool/write_qr.dart', - '-o', - bmpPath, - if (config.version != null) ...['-v', config.version.toString()], - if (config.correction != null) ...['-c', config.correction!], - '--scale', - '10', - input, - ]; - - final process = await TestProcess.start('dart', args); - await process.shouldExit(0); - - expect( - File(bmpPath).existsSync(), - isTrue, - reason: 'BMP file should be created', - ); - - // Validate with zbarimg - // zbarimg output format: QR-Code:content - final zbar = await TestProcess.start('zbarimg', ['--quiet', bmpPath]); - await zbar.shouldExit(0); - final output = (await zbar.stdout.rest.toList()).join('\n').trim(); - - if (output != 'QR-Code:$input') { - print('zbarimg failed to match input.'); - print('Input: $input'); - print('Output: "$output"'); - } - expect(output, 'QR-Code:$input'); - }, - timeout: const Timeout(Duration(seconds: 20)), - ); - } - } - - test('Generate QR with Version 1 (numeric input)', () async { - const input = '123456'; - final bmpPath = p.join(tempDir.path, 'test_v1_numeric.bmp'); - final args = [ - 'tool/write_qr.dart', - '-o', - bmpPath, - '-v', - '1', - '-c', - 'L', - '--scale', - '10', - input, - ]; - - final process = await TestProcess.start('dart', args); - await process.shouldExit(0); - expect( - File(bmpPath).existsSync(), - isTrue, - reason: 'BMP file should be created', - ); - - final zbar = await TestProcess.start('zbarimg', ['--quiet', bmpPath]); - await zbar.shouldExit(0); - final output = (await zbar.stdout.rest.toList()).join('\n').trim(); - - if (output != 'QR-Code:$input') { - print('zbarimg failed to match input.'); - print('Input: $input'); - print('Output: "$output"'); - } - expect(output, 'QR-Code:$input'); - }); - - test('Error case: Missing output argument', () async { - final process = await TestProcess.start('dart', [ - 'tool/write_qr.dart', - 'content', - ]); - await process.shouldExit(1); - final output = await process.stdout.next; - expect( - output, - contains('Error: Invalid argument(s): Option output is mandatory.'), - ); - }); - - test('Error case: Invalid version', () async { - final bmpPath = p.join(tempDir.path, 'invalid_version.bmp'); - final process = await TestProcess.start('dart', [ - 'tool/write_qr.dart', - '-o', - bmpPath, - '-v', - '41', - 'content', - ]); - await process.shouldExit(1); - }); - - test('Error case: Invalid correction', () async { - final bmpPath = p.join(tempDir.path, 'invalid_correction.bmp'); - final process = await TestProcess.start('dart', [ - 'tool/write_qr.dart', - '-o', - bmpPath, - '-c', - 'X', - 'content', - ]); - await process.shouldExit(1); // ArgParser error - }); - - test('Error case: Input too long for version (explicit version)', () async { - const input = - 'This string is definitely too long for Version 1 with ' - 'High error correction level.'; - final bmpPath = p.join(tempDir.path, 'too_long.bmp'); - final process = await TestProcess.start('dart', [ - 'tool/write_qr.dart', - '-o', - bmpPath, - '-v', - '1', - '-c', - 'H', // High error correction reduces capacity - input, - ]); - await process.shouldExit(1); - }); -}