downgrade qr

This commit is contained in:
otsmr 2026-02-12 23:03:46 +01:00
parent 785dccdf9c
commit 33111edeb2
23 changed files with 360 additions and 1656 deletions

View file

@ -13,7 +13,7 @@ no_screenshot: 9ca2a492ff12e5179583a1fa015bf0843382b866
optional: 71c638891ce4f2aff35c7387727989f31f9d877d optional: 71c638891ce4f2aff35c7387727989f31f9d877d
photo_view: a13ca2fc387a3fb1276126959e092c44d0029987 photo_view: a13ca2fc387a3fb1276126959e092c44d0029987
pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
qr: 5fa01fcccd6121b906dc7df4fffa9fa22ca94f75 qr: 7b1e9665ca976f484e7975356cf26fc7a0ccf02e
qr_flutter: d5e7206396105d643113618290bbcc755d05f492 qr_flutter: d5e7206396105d643113618290bbcc755d05f492
restart_app: 12339f63bf8e9631e619c4f9f6b4e013fa324715 restart_app: 12339f63bf8e9631e619c4f9f6b4e013fa324715
x25519: ecb1d357714537bba6e276ef45f093846d4beaee x25519: ecb1d357714537bba6e276ef45f093846d4beaee

View file

@ -1,9 +1,5 @@
export 'src/bit_buffer.dart'; 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/error_correct_level.dart';
export 'src/input_too_long_exception.dart'; export 'src/input_too_long_exception.dart';
export 'src/mode.dart';
export 'src/qr_code.dart'; export 'src/qr_code.dart';
export 'src/qr_image.dart'; export 'src/qr_image.dart';

View file

@ -1,78 +1,34 @@
/// A growable sequence of bits. import 'dart:collection';
///
/// Used internally to construct the data bit stream for a QR code. class QrBitBuffer extends Object with ListMixin<bool> {
class QrBitBuffer extends Iterable<bool> { final List<int> _buffer;
final _buffer = <int>[];
int _length = 0; int _length = 0;
QrBitBuffer(); QrBitBuffer() : _buffer = <int>[];
@override @override
int get length => _length; void operator []=(int index, bool value) =>
throw UnsupportedError('cannot change');
@override @override
Iterator<bool> get iterator => _QrBitBufferIterator(this);
bool operator [](int index) { bool operator [](int index) {
final bufIndex = index ~/ 8; final bufIndex = index ~/ 8;
return ((_buffer[bufIndex] >> (7 - index % 8)) & 1) == 1; 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]; int getByte(int index) => _buffer[index];
void put(int number, int length) { void put(int number, int length) {
if (length == 0) return; for (var i = 0; i < length; i++) {
final bit = ((number >> (length - i - 1)) & 1) == 1;
var bitIndex = _length; putBit(bit);
final endBitIndex = bitIndex + length;
// Ensure capacity
final neededBytes = (endBitIndex + 7) >> 3; // (endBitIndex + 7) ~/ 8
while (_buffer.length < neededBytes) {
_buffer.add(0);
} }
// 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) { void putBit(bool bit) {
@ -87,28 +43,4 @@ class QrBitBuffer extends Iterable<bool> {
_length++; _length++;
} }
List<bool> getRange(int start, int end) {
final list = <bool>[];
for (var i = start; i < end; i++) {
list.add(this[i]);
}
return list;
}
}
class _QrBitBufferIterator implements Iterator<bool> {
final QrBitBuffer _buffer;
int _currentIndex = -1;
_QrBitBufferIterator(this._buffer);
@override
bool get current => _buffer[_currentIndex];
@override
bool moveNext() {
_currentIndex++;
return _currentIndex < _buffer.length;
}
} }

View file

@ -2,47 +2,17 @@ import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'bit_buffer.dart'; import 'bit_buffer.dart';
import 'eci.dart'; import 'mode.dart' as qr_mode;
import 'mode.dart';
/// A piece of data to be encoded in a QR code.
///
/// Use [toDatums] to parse a string into optimal segments.
abstract class QrDatum { abstract class QrDatum {
QrMode get mode; int get mode;
int get length; int get length;
void write(QrBitBuffer buffer); 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<QrDatum> 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 { class QrByte implements QrDatum {
@override @override
final QrMode mode = QrMode.byte; final int mode = qr_mode.mode8bitByte;
final Uint8List _data; final Uint8List _data;
factory QrByte(String input) => factory QrByte(String input) =>
@ -64,20 +34,13 @@ class QrByte implements QrDatum {
} }
} }
/// Encodes numeric data (digits 0-9). /// Encodes numbers (0-9) 10 bits per 3 digits.
///
/// Compresses 3 digits into 10 bits.
/// Most efficient mode for decimal numbers.
class QrNumeric implements QrDatum { class QrNumeric implements QrDatum {
static final RegExp validationRegex = RegExp(r'^[0-9]+$'); static final RegExp validationRegex = RegExp(r'^[0-9]+$');
factory QrNumeric.fromString(String numberString) { factory QrNumeric.fromString(String numberString) {
if (!validationRegex.hasMatch(numberString)) { if (!validationRegex.hasMatch(numberString)) {
throw ArgumentError.value( throw ArgumentError('string can only contain digits 0-9');
numberString,
'numberString',
'string can only contain digits 0-9',
);
} }
final newList = Uint8List(numberString.length); final newList = Uint8List(numberString.length);
var count = 0; var count = 0;
@ -92,7 +55,7 @@ class QrNumeric implements QrDatum {
final Uint8List _data; final Uint8List _data;
@override @override
final QrMode mode = QrMode.numeric; final int mode = qr_mode.modeNumber;
@override @override
void write(QrBitBuffer buffer) { void write(QrBitBuffer buffer) {
@ -119,10 +82,7 @@ class QrNumeric implements QrDatum {
int get length => _data.length; int get length => _data.length;
} }
/// Encodes alphanumeric data (uppercase letters, digits, and specific symbols). /// Encodes numbers (0-9) 10 bits per 3 digits.
///
/// Supported characters: 0-9, A-Z, space, $, %, *, +, -, ., /, :
/// Compresses 2 characters into 11 bits.
class QrAlphaNumeric implements QrDatum { class QrAlphaNumeric implements QrDatum {
static const alphaNumTable = r'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:'; static const alphaNumTable = r'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
// Note: '-' anywhere in this string is a range character. // Note: '-' anywhere in this string is a range character.
@ -142,10 +102,9 @@ class QrAlphaNumeric implements QrDatum {
factory QrAlphaNumeric.fromString(String alphaNumeric) { factory QrAlphaNumeric.fromString(String alphaNumeric) {
if (!alphaNumeric.contains(validationRegex)) { if (!alphaNumeric.contains(validationRegex)) {
throw ArgumentError.value( throw ArgumentError(
alphaNumeric, 'String does not contain valid ALPHA-NUM '
'alphaNumeric', 'character set: $alphaNumeric',
'String does not contain valid ALPHA-NUM character set',
); );
} }
return QrAlphaNumeric._(alphaNumeric); return QrAlphaNumeric._(alphaNumeric);
@ -154,7 +113,7 @@ class QrAlphaNumeric implements QrDatum {
QrAlphaNumeric._(this._string); QrAlphaNumeric._(this._string);
@override @override
final QrMode mode = QrMode.alphaNumeric; final int mode = qr_mode.modeAlphaNum;
@override @override
void write(QrBitBuffer buffer) { void write(QrBitBuffer buffer) {

View file

@ -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);
}
}
}

View file

@ -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);
}

View file

@ -1,27 +1,20 @@
/// QR Code error correction level. // ignore: avoid_classes_with_only_static_members
/// class QrErrorCorrectLevel {
/// Recover capacity: static const int L = 1;
/// * [low] : ~7% static const int M = 0;
/// * [medium] : ~15% static const int Q = 3;
/// * [quartile] : ~25% static const int H = 2;
/// * [high] : ~30%
enum QrErrorCorrectLevel {
// NOTE: the order here MATTERS.
// The index maps to the QR standard.
/// Level M (Medium) ~15% error correction. // thesee *are* in order of lowest to highest quality...I think
medium(15), // 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<int> levels = [L, M, Q, H];
/// Level L (Low) ~7% error correction. static String getName(int level) => switch (level) {
low(7), L => 'Low',
M => 'Medium',
/// Level H (High) ~30% error correction. Q => 'Quartile',
high(30), H => 'High',
_ => throw ArgumentError('level $level not supported'),
/// Level Q (Quartile) ~25% error correction. };
quartile(25);
final int recoveryRate;
const QrErrorCorrectLevel(this.recoveryRate);
} }

View file

@ -1,25 +1,8 @@
enum QrMaskPattern { const int pattern000 = 0;
pattern000(_check000), const int pattern001 = 1;
pattern001(_check001), const int pattern010 = 2;
pattern010(_check010), const int pattern011 = 3;
pattern011(_check011), const int pattern100 = 4;
pattern100(_check100), const int pattern101 = 5;
pattern101(_check101), const int pattern110 = 6;
pattern110(_check110), const int pattern111 = 7;
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;

View file

@ -3,8 +3,7 @@ import 'dart:typed_data';
final Uint8List _logTable = _createLogTable(); final Uint8List _logTable = _createLogTable();
final Uint8List _expTable = _createExpTable(); final Uint8List _expTable = _createExpTable();
int glog(int n) => int glog(int n) => (n >= 1) ? _logTable[n] : throw ArgumentError('glog($n)');
(n >= 1) ? _logTable[n] : throw ArgumentError.value(n, 'n', 'must be >= 1');
int gexp(int n) => _expTable[n % 255]; int gexp(int n) => _expTable[n % 255];

View file

@ -1,55 +1,4 @@
/// The encoding mode of a QR code segment. const int modeNumber = 1 << 0;
enum QrMode { const int modeAlphaNum = 1 << 1;
/// Numeric mode (0-9). Most efficient. const int mode8bitByte = 1 << 2;
numeric(1), const int modeKanji = 1 << 3;
/// 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,
};
}
}
}

View file

@ -28,75 +28,36 @@ class QrPolynomial {
int get length => _values.length; int get length => _values.length;
QrPolynomial multiply(QrPolynomial e) { QrPolynomial multiply(QrPolynomial e) {
final foo = Uint8List(length + e.length - 1); final List<int> foo = Uint8List(length + e.length - 1);
for (var i = 0; i < length; i++) { 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++) { for (var j = 0; j < e.length; j++) {
final v2 = e[j]; foo[i + j] ^= qr_math.gexp(qr_math.glog(this[i]) + qr_math.glog(e[j]));
if (v2 == 0) continue;
foo[i + j] ^= qr_math.gexp(log1 + qr_math.glog(v2));
} }
} }
return QrPolynomial._internal(foo); return QrPolynomial(foo, 0);
} }
QrPolynomial mod(QrPolynomial e) { QrPolynomial mod(QrPolynomial e) {
if (length - e.length < 0) { if (length - e.length < 0) {
// ignore: avoid_returning_this
return this; return this;
} }
// Use a copy of _values that we will mutate final ratio = qr_math.glog(this[0]) - qr_math.glog(e[0]);
// 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 values = Uint8List.fromList(_values); final value = Uint8List(length);
for (var i = 0; i < values.length - e.length + 1; i++) { for (var i = 0; i < length; i++) {
final v = values[i]; value[i] = this[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);
}
} }
// The result is the remainder, which is the last e.length - 1 coefficients? for (var i = 0; i < e.length; i++) {
// Wait, the degree of remainder is less than degree of divisor (e). value[i] ^= qr_math.gexp(qr_math.glog(e[i]) + ratio);
// e.length is e.degree + 1. }
// So remainder length is e.length - 1.
// Find where the remainder starts. // recursive call
// In the loop above, we zeroed out terms from 0 to return QrPolynomial(value, 0).mod(e);
// `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);
} }
} }

View file

@ -5,61 +5,66 @@ import 'package:meta/meta.dart';
import 'bit_buffer.dart'; import 'bit_buffer.dart';
import 'byte.dart'; import 'byte.dart';
import 'eci.dart';
import 'error_correct_level.dart'; import 'error_correct_level.dart';
import 'input_too_long_exception.dart'; import 'input_too_long_exception.dart';
import 'math.dart' as qr_math; import 'math.dart' as qr_math;
import 'mode.dart' as qr_mode;
import 'polynomial.dart'; import 'polynomial.dart';
import 'rs_block.dart'; import 'rs_block.dart';
class QrCode { class QrCode {
final int typeNumber; final int typeNumber;
final QrErrorCorrectLevel errorCorrectLevel; final int errorCorrectLevel;
final int moduleCount; final int moduleCount;
List<int>? _dataCache; List<int>? _dataCache;
final _dataList = <QrDatum>[]; final _dataList = <QrDatum>[];
QrCode(this.typeNumber, this.errorCorrectLevel) QrCode(this.typeNumber, this.errorCorrectLevel)
: moduleCount = typeNumber * 4 + 17 { : 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.checkValueInInterval(typeNumber, 1, 40, 'typeNumber');
RangeError.checkValidIndex(
errorCorrectLevel,
QrErrorCorrectLevel.levels,
'errorCorrectLevel',
);
} }
factory QrCode.fromData({ factory QrCode.fromData({
required String data, required String data,
required QrErrorCorrectLevel errorCorrectLevel, required int errorCorrectLevel,
}) { }) {
final datumList = QrDatum.toDatums(data); final QrDatum datum;
// Automatically determine mode here
final typeNumber = _calculateTypeNumberFromData( if (QrNumeric.validationRegex.hasMatch(data)) {
errorCorrectLevel, // Numeric mode for numbers only
datumList, datum = QrNumeric.fromString(data);
); } else if (QrAlphaNumeric.validationRegex.hasMatch(data)) {
// Alphanumeric mode for alphanumeric characters only
final qrCode = QrCode(typeNumber, errorCorrectLevel); datum = QrAlphaNumeric.fromString(data);
for (final datum in datumList) { } else {
qrCode._addToList(datum); // Default to byte mode for other characters
datum = QrByte(data);
} }
final typeNumber = _calculateTypeNumberFromData(errorCorrectLevel, datum);
final qrCode = QrCode(typeNumber, errorCorrectLevel).._addToList(datum);
return qrCode; return qrCode;
} }
factory QrCode.fromUint8List({ factory QrCode.fromUint8List({
required Uint8List data, required Uint8List data,
required QrErrorCorrectLevel errorCorrectLevel, required int errorCorrectLevel,
}) { }) {
final datum = QrByte.fromUint8List(data); final typeNumber = _calculateTypeNumberFromData(
final typeNumber = _calculateTypeNumberFromData(errorCorrectLevel, [datum]); errorCorrectLevel,
return QrCode(typeNumber, errorCorrectLevel).._addToList(datum); QrByte.fromUint8List(data),
);
return QrCode(typeNumber, errorCorrectLevel)
.._addToList(QrByte.fromUint8List(data));
} }
static int _calculateTotalDataBits( static int _calculateTotalDataBits(int typeNumber, int errorCorrectLevel) {
int typeNumber,
QrErrorCorrectLevel errorCorrectLevel,
) {
final rsBlocks = QrRsBlock.getRSBlocks(typeNumber, errorCorrectLevel); final rsBlocks = QrRsBlock.getRSBlocks(typeNumber, errorCorrectLevel);
var totalDataBits = 0; var totalDataBits = 0;
for (var rsBlock in rsBlocks) { for (var rsBlock in rsBlocks) {
@ -68,35 +73,26 @@ class QrCode {
return totalDataBits; return totalDataBits;
} }
static int _calculateTypeNumberFromData( static int _calculateTypeNumberFromData(int errorCorrectLevel, QrDatum data) {
QrErrorCorrectLevel errorCorrectLevel,
List<QrDatum> data,
) {
for (var typeNumber = 1; typeNumber <= 40; typeNumber++) { for (var typeNumber = 1; typeNumber <= 40; typeNumber++) {
final totalDataBits = _calculateTotalDataBits( final totalDataBits = _calculateTotalDataBits(
typeNumber, typeNumber,
errorCorrectLevel, errorCorrectLevel,
); );
final buffer = QrBitBuffer(); final buffer = QrBitBuffer()
for (final datum in data) { ..put(data.mode, 4)
buffer ..put(data.length, _lengthInBits(data.mode, typeNumber));
..put(datum.mode.value, 4) data.write(buffer);
..put(datum.length, datum.mode.getLengthBits(typeNumber));
datum.write(buffer);
}
if (buffer.length <= totalDataBits) return typeNumber; if (buffer.length <= totalDataBits) return typeNumber;
} }
// If we reach here, the data is too long for any QR Code version. // If we reach here, the data is too long for any QR Code version.
final buffer = QrBitBuffer(); final buffer = QrBitBuffer()
for (final datum in data) { ..put(data.mode, 4)
buffer ..put(data.length, _lengthInBits(data.mode, 40));
..put(datum.mode.value, 4) data.write(buffer);
..put(datum.length, datum.mode.getLengthBits(40));
datum.write(buffer);
}
final maxBits = _calculateTotalDataBits(40, errorCorrectLevel); final maxBits = _calculateTotalDataBits(40, errorCorrectLevel);
@ -104,9 +100,19 @@ class QrCode {
} }
void addData(String data) { void addData(String data) {
for (final datum in QrDatum.toDatums(data)) { final QrDatum datum;
_addToList(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)); void addByteData(ByteData data) => _addToList(QrByte.fromByteData(data));
@ -121,8 +127,6 @@ class QrCode {
void addAlphaNumeric(String alphaNumeric) => void addAlphaNumeric(String alphaNumeric) =>
_addToList(QrAlphaNumeric.fromString(alphaNumeric)); _addToList(QrAlphaNumeric.fromString(alphaNumeric));
void addECI(int eciValue) => _addToList(QrEci(eciValue));
void _addToList(QrDatum data) { void _addToList(QrDatum data) {
_dataList.add(data); _dataList.add(data);
_dataCache = null; _dataCache = null;
@ -138,7 +142,7 @@ const int _pad1 = 0x11;
List<int> _createData( List<int> _createData(
int typeNumber, int typeNumber,
QrErrorCorrectLevel errorCorrectLevel, int errorCorrectLevel,
List<QrDatum> dataList, List<QrDatum> dataList,
) { ) {
final rsBlocks = QrRsBlock.getRSBlocks(typeNumber, errorCorrectLevel); final rsBlocks = QrRsBlock.getRSBlocks(typeNumber, errorCorrectLevel);
@ -148,8 +152,8 @@ List<int> _createData(
for (var i = 0; i < dataList.length; i++) { for (var i = 0; i < dataList.length; i++) {
final data = dataList[i]; final data = dataList[i];
buffer buffer
..put(data.mode.value, 4) ..put(data.mode, 4)
..put(data.length, data.mode.getLengthBits(typeNumber)); ..put(data.length, _lengthInBits(data.mode, typeNumber));
data.write(buffer); data.write(buffer);
} }
@ -160,10 +164,6 @@ List<int> _createData(
errorCorrectLevel, errorCorrectLevel,
); );
if (buffer.length > totalDataBits) {
throw InputTooLongException(buffer.length, totalDataBits);
}
// HUH? // HUH?
// è[É[Éh // è[É[Éh
if (buffer.length + 4 <= totalDataBits) { if (buffer.length + 4 <= totalDataBits) {
@ -244,6 +244,39 @@ List<int> _createBytes(QrBitBuffer buffer, List<QrRsBlock> rsBlocks) {
return data; 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) { QrPolynomial _errorCorrectPolynomial(int errorCorrectLength) {
var a = QrPolynomial([1], 0); var a = QrPolynomial([1], 0);

View file

@ -1,72 +1,34 @@
import 'dart:typed_data';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'error_correct_level.dart'; import 'mask_pattern.dart' as qr_mask_pattern;
import 'mask_pattern.dart';
import 'qr_code.dart'; import 'qr_code.dart';
import 'util.dart' as qr_util; import 'util.dart' as qr_util;
/// Renders the encoded data from a [QrCode] in a portable format. /// Renders the encoded data from a [QrCode] in a portable format.
class QrImage { class QrImage {
static const _pixelUnassigned = 0;
static const _pixelLight = 1;
static const _pixelDark = 2;
final int moduleCount; final int moduleCount;
final int typeNumber; final int typeNumber;
final QrErrorCorrectLevel errorCorrectLevel; final int errorCorrectLevel;
final int maskPattern; final int maskPattern;
final Uint8List _data; final _modules = <List<bool?>>[];
/// Generates a QrImage with the best mask pattern encoding [qrCode]. /// Generates a QrImage with the best mask pattern encoding [qrCode].
factory QrImage(QrCode qrCode) { factory QrImage(QrCode qrCode) {
// Create a template with invariant patterns var minLostPoint = 0.0;
final template = QrImage._template(qrCode); QrImage? bestImage;
final moduleCount = template.moduleCount;
final dataSize = moduleCount * moduleCount;
// 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++) { for (var i = 0; i < 8; i++) {
// Copy pre-placed data to working buffer final testImage = QrImage._test(qrCode, i);
workingBuffer.setRange(0, dataSize, dataMap);
final testImage = QrImage._fromData(qrCode, i, workingBuffer)
// Apply mask (XOR)
.._applyMask(QrMaskPattern.values[i], template._data);
final lostPoint = _lostPoint(testImage); final lostPoint = _lostPoint(testImage);
if (lostPoint < minLostPoint) { if (i == 0 || minLostPoint > lostPoint) {
minLostPoint = lostPoint; minLostPoint = lostPoint;
bestMaskPattern = i; bestImage = testImage;
// Copy working buffer to bestData
bestData ??= Uint8List(dataSize);
bestData.setRange(0, dataSize, workingBuffer);
} }
} }
final finalImage = QrImage._fromData(qrCode, bestMaskPattern, bestData!) return QrImage.withMaskPattern(qrCode, bestImage!.maskPattern);
// Final setup with correct format info (not test, so actual pixels)
.._setupTypeInfo(bestMaskPattern, false);
if (finalImage.typeNumber >= 7) {
finalImage._setupTypeNumber(false);
}
return finalImage;
} }
/// Generates a specific image for the [qrCode] and [maskPattern]. /// Generates a specific image for the [qrCode] and [maskPattern].
@ -74,75 +36,35 @@ class QrImage {
: assert(maskPattern >= 0 && maskPattern <= 7), : assert(maskPattern >= 0 && maskPattern <= 7),
moduleCount = qrCode.moduleCount, moduleCount = qrCode.moduleCount,
typeNumber = qrCode.typeNumber, typeNumber = qrCode.typeNumber,
errorCorrectLevel = qrCode.errorCorrectLevel, errorCorrectLevel = qrCode.errorCorrectLevel {
_data = Uint8List(qrCode.moduleCount * qrCode.moduleCount) {
_makeImpl(maskPattern, qrCode.dataCache, false); _makeImpl(maskPattern, qrCode.dataCache, false);
} }
/// Internal constructor for template creation QrImage._test(QrCode qrCode, this.maskPattern)
QrImage._template(QrCode qrCode)
: moduleCount = qrCode.moduleCount, : moduleCount = qrCode.moduleCount,
typeNumber = qrCode.typeNumber, typeNumber = qrCode.typeNumber,
errorCorrectLevel = qrCode.errorCorrectLevel, errorCorrectLevel = qrCode.errorCorrectLevel {
maskPattern = 0, // Irrelevant _makeImpl(maskPattern, qrCode.dataCache, true);
_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);
} }
}
/// Internal constructor for testing phase
QrImage._fromData(QrCode qrCode, this.maskPattern, this._data)
: moduleCount = qrCode.moduleCount,
typeNumber = qrCode.typeNumber,
errorCorrectLevel = qrCode.errorCorrectLevel;
@visibleForTesting @visibleForTesting
List<List<bool?>> get qrModules { List<List<bool?>> get qrModules => _modules;
final list = <List<bool?>>[];
for (var r = 0; r < moduleCount; r++) {
final row = List<bool?>.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;
}
void _resetModules() { void _resetModules() {
_data.fillRange(0, _data.length, _pixelUnassigned); _modules.clear();
for (var row = 0; row < moduleCount; row++) {
_modules.add(List<bool?>.filled(moduleCount, null));
}
} }
bool isDark(int row, int col) { bool isDark(int row, int col) {
if (row < 0 || moduleCount <= row) { if (row < 0 || moduleCount <= row || col < 0 || moduleCount <= col) {
throw RangeError.range(row, 0, moduleCount - 1, 'row'); throw ArgumentError('$row , $col');
} }
if (col < 0 || moduleCount <= col) { return _modules[row][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;
} }
void _makeImpl(int maskPattern, List<int> dataCache, bool test) { void _makeImpl(int maskPattern, List<int> 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(); _resetModules();
_setupPositionProbePattern(0, 0); _setupPositionProbePattern(0, 0);
_setupPositionProbePattern(moduleCount - 7, 0); _setupPositionProbePattern(moduleCount - 7, 0);
@ -158,13 +80,6 @@ class QrImage {
_mapData(dataCache, maskPattern); _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) { void _setupPositionProbePattern(int row, int col) {
for (var r = -1; r <= 7; r++) { for (var r = -1; r <= 7; r++) {
if (row + r <= -1 || moduleCount <= row + r) continue; if (row + r <= -1 || moduleCount <= row + r) continue;
@ -175,9 +90,9 @@ class QrImage {
if ((0 <= r && r <= 6 && (c == 0 || c == 6)) || if ((0 <= r && r <= 6 && (c == 0 || c == 6)) ||
(0 <= c && c <= 6 && (r == 0 || r == 6)) || (0 <= c && c <= 6 && (r == 0 || r == 6)) ||
(2 <= r && r <= 4 && 2 <= c && c <= 4)) { (2 <= r && r <= 4 && 2 <= c && c <= 4)) {
_set(row + r, col + c, true); _modules[row + r][col + c] = true;
} else { } else {
_set(row + r, col + c, false); _modules[row + r][col + c] = false;
} }
} }
} }
@ -191,16 +106,16 @@ class QrImage {
final row = pos[i]; final row = pos[i];
final col = pos[j]; final col = pos[j];
if (_data[row * moduleCount + col] != _pixelUnassigned) { if (_modules[row][col] != null) {
continue; continue;
} }
for (var r = -2; r <= 2; r++) { for (var r = -2; r <= 2; r++) {
for (var c = -2; c <= 2; c++) { for (var c = -2; c <= 2; c++) {
if (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) { 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 { } else {
_set(row + r, col + c, false); _modules[row + r][col + c] = false;
} }
} }
} }
@ -210,22 +125,22 @@ class QrImage {
void _setupTimingPattern() { void _setupTimingPattern() {
for (var r = 8; r < moduleCount - 8; r++) { for (var r = 8; r < moduleCount - 8; r++) {
if (_data[r * moduleCount + 6] != _pixelUnassigned) { if (_modules[r][6] != null) {
continue; continue;
} }
_set(r, 6, r.isEven); _modules[r][6] = r.isEven;
} }
for (var c = 8; c < moduleCount - 8; c++) { for (var c = 8; c < moduleCount - 8; c++) {
if (_data[6 * moduleCount + c] != _pixelUnassigned) { if (_modules[6][c] != null) {
continue; continue;
} }
_set(6, c, c.isEven); _modules[6][c] = c.isEven;
} }
} }
void _setupTypeInfo(int maskPattern, bool test) { void _setupTypeInfo(int maskPattern, bool test) {
final data = (errorCorrectLevel.index << 3) | maskPattern; final data = (errorCorrectLevel << 3) | maskPattern;
final bits = qr_util.bchTypeInfo(data); final bits = qr_util.bchTypeInfo(data);
int i; int i;
@ -236,11 +151,11 @@ class QrImage {
mod = !test && ((bits >> i) & 1) == 1; mod = !test && ((bits >> i) & 1) == 1;
if (i < 6) { if (i < 6) {
_set(i, 8, mod); _modules[i][8] = mod;
} else if (i < 8) { } else if (i < 8) {
_set(i + 1, 8, mod); _modules[i + 1][8] = mod;
} else { } 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; mod = !test && ((bits >> i) & 1) == 1;
if (i < 8) { if (i < 8) {
_set(8, moduleCount - i - 1, mod); _modules[8][moduleCount - i - 1] = mod;
} else if (i < 9) { } else if (i < 9) {
_set(8, 15 - i - 1 + 1, mod); _modules[8][15 - i - 1 + 1] = mod;
} else { } else {
_set(8, 15 - i - 1, mod); _modules[8][15 - i - 1] = mod;
} }
} }
// fixed module // fixed module
_set(moduleCount - 8, 8, !test); _modules[moduleCount - 8][8] = !test;
} }
void _setupTypeNumber(bool test) { void _setupTypeNumber(bool test) {
@ -266,12 +181,12 @@ class QrImage {
for (var i = 0; i < 18; i++) { for (var i = 0; i < 18; i++) {
final mod = !test && ((bits >> i) & 1) == 1; 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++) { for (var i = 0; i < 18; i++) {
final mod = !test && ((bits >> i) & 1) == 1; 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 (;;) {
for (var c = 0; c < 2; c++) { for (var c = 0; c < 2; c++) {
if (_data[row * moduleCount + (col - c)] == _pixelUnassigned) { if (_modules[row][col - c] == null) {
var dark = false; var dark = false;
if (byteIndex < data.length) { if (byteIndex < data.length) {
dark = ((data[byteIndex] >> bitIndex) & 1) == 1; dark = ((data[byteIndex] >> bitIndex) & 1) == 1;
} }
final mask = QrMaskPattern.values[maskPattern].check(row, col - c); final mask = _mask(maskPattern, row, col - c);
if (mask) { if (mask) {
dark = !dark; dark = !dark;
} }
_set(row, col - c, dark); _modules[row][col - c] = dark;
bitIndex--; bitIndex--;
if (bitIndex == -1) { if (bitIndex == -1) {
@ -319,123 +234,49 @@ class QrImage {
} }
} }
} }
void _placeData(List<int> 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) { double _lostPoint(QrImage qrImage) {
final moduleCount = qrImage.moduleCount; final moduleCount = qrImage.moduleCount;
final data = qrImage._data;
var lostPoint = 0.0; var lostPoint = 0.0;
int row, col;
// Cache data length for faster access (though it's final) // LEVEL1
// Accessing local vars is faster. for (row = 0; row < moduleCount; row++) {
for (col = 0; col < moduleCount; col++) {
// Level 1
for (var row = 0; row < moduleCount; row++) {
for (var col = 0; col < moduleCount; col++) {
var sameCount = 0; var sameCount = 0;
final currentIdx = row * moduleCount + col; final dark = qrImage.isDark(row, col);
final isDark = data[currentIdx] == QrImage._pixelDark;
// Check all 8 neighbors for (var r = -1; r <= 1; r++) {
// Top row if (row + r < 0 || moduleCount <= row + r) {
if (row > 0) { continue;
final upIdx = currentIdx - moduleCount;
if (col > 0 && (data[upIdx - 1] == QrImage._pixelDark) == isDark) {
sameCount++;
}
if ((data[upIdx] == QrImage._pixelDark) == isDark) sameCount++;
if (col < moduleCount - 1 &&
(data[upIdx + 1] == QrImage._pixelDark) == isDark) {
sameCount++;
}
} }
// Middle row (left/right) for (var c = -1; c <= 1; c++) {
if (col > 0 && (data[currentIdx - 1] == QrImage._pixelDark) == isDark) { if (col + c < 0 || moduleCount <= col + c) {
sameCount++; continue;
}
if (col < moduleCount - 1 &&
(data[currentIdx + 1] == QrImage._pixelDark) == isDark) {
sameCount++;
} }
// Bottom row if (r == 0 && c == 0) {
if (row < moduleCount - 1) { continue;
final downIdx = currentIdx + moduleCount; }
if (col > 0 && (data[downIdx - 1] == QrImage._pixelDark) == isDark) {
if (dark == qrImage.isDark(row + r, col + c)) {
sameCount++; sameCount++;
} }
if ((data[downIdx] == QrImage._pixelDark) == isDark) sameCount++;
if (col < moduleCount - 1 &&
(data[downIdx + 1] == QrImage._pixelDark) == isDark) {
sameCount++;
} }
} }
@ -445,58 +286,58 @@ double _lostPoint(QrImage qrImage) {
} }
} }
// Level 2: 2x2 blocks of same color // LEVEL2
for (var row = 0; row < moduleCount - 1; row++) { for (row = 0; row < moduleCount - 1; row++) {
for (var col = 0; col < moduleCount - 1; col++) { for (col = 0; col < moduleCount - 1; col++) {
final idx = row * moduleCount + col; var count = 0;
final p00 = data[idx]; if (qrImage.isDark(row, col)) count++;
final p01 = data[idx + 1]; if (qrImage.isDark(row + 1, col)) count++;
final p10 = data[idx + moduleCount]; if (qrImage.isDark(row, col + 1)) count++;
final p11 = data[idx + moduleCount + 1]; if (qrImage.isDark(row + 1, col + 1)) count++;
if (count == 0 || count == 4) {
if (p00 == p01 && p00 == p10 && p00 == p11) {
lostPoint += 3; lostPoint += 3;
} }
} }
} }
// Level 3: 1:1:3:1:1 pattern // LEVEL3
// Dark, Light, Dark, Dark, Dark, Light, Dark for (row = 0; row < moduleCount; row++) {
for (var row = 0; row < moduleCount; row++) { for (col = 0; col < moduleCount - 6; col++) {
for (var col = 0; col < moduleCount - 6; col++) { if (qrImage.isDark(row, col) &&
final idx = row * moduleCount + col; !qrImage.isDark(row, col + 1) &&
if (data[idx] == QrImage._pixelDark && qrImage.isDark(row, col + 2) &&
data[idx + 1] == QrImage._pixelLight && qrImage.isDark(row, col + 3) &&
data[idx + 2] == QrImage._pixelDark && qrImage.isDark(row, col + 4) &&
data[idx + 3] == QrImage._pixelDark && !qrImage.isDark(row, col + 5) &&
data[idx + 4] == QrImage._pixelDark && qrImage.isDark(row, col + 6)) {
data[idx + 5] == QrImage._pixelLight &&
data[idx + 6] == QrImage._pixelDark) {
lostPoint += 40; lostPoint += 40;
} }
} }
} }
// Check cols for (col = 0; col < moduleCount; col++) {
for (var col = 0; col < moduleCount; col++) { for (row = 0; row < moduleCount - 6; row++) {
for (var row = 0; row < moduleCount - 6; row++) { if (qrImage.isDark(row, col) &&
final idx = row * moduleCount + col; !qrImage.isDark(row + 1, col) &&
if (data[idx] == QrImage._pixelDark && qrImage.isDark(row + 2, col) &&
data[idx + moduleCount] == QrImage._pixelLight && qrImage.isDark(row + 3, col) &&
data[idx + 2 * moduleCount] == QrImage._pixelDark && qrImage.isDark(row + 4, col) &&
data[idx + 3 * moduleCount] == QrImage._pixelDark && !qrImage.isDark(row + 5, col) &&
data[idx + 4 * moduleCount] == QrImage._pixelDark && qrImage.isDark(row + 6, col)) {
data[idx + 5 * moduleCount] == QrImage._pixelLight &&
data[idx + 6 * moduleCount] == QrImage._pixelDark) {
lostPoint += 40; lostPoint += 40;
} }
} }
} }
// Level 4: Dark ratio // LEVEL4
var darkCount = 0; 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; final ratio = (100 * darkCount / moduleCount / moduleCount - 50).abs() / 5;

View file

@ -6,10 +6,7 @@ class QrRsBlock {
QrRsBlock._(this.totalCount, this.dataCount); QrRsBlock._(this.totalCount, this.dataCount);
static List<QrRsBlock> getRSBlocks( static List<QrRsBlock> getRSBlocks(int typeNumber, int errorCorrectLevel) {
int typeNumber,
QrErrorCorrectLevel errorCorrectLevel,
) {
final rsBlock = _getRsBlockTable(typeNumber, errorCorrectLevel); final rsBlock = _getRsBlockTable(typeNumber, errorCorrectLevel);
final length = rsBlock.length ~/ 3; final length = rsBlock.length ~/ 3;
@ -32,12 +29,15 @@ class QrRsBlock {
List<int> _getRsBlockTable( List<int> _getRsBlockTable(
int typeNumber, int typeNumber,
QrErrorCorrectLevel errorCorrectLevel, int errorCorrectLevel,
) => switch (errorCorrectLevel) { ) => switch (errorCorrectLevel) {
QrErrorCorrectLevel.low => _rsBlockTable[(typeNumber - 1) * 4 + 0], QrErrorCorrectLevel.L => _rsBlockTable[(typeNumber - 1) * 4 + 0],
QrErrorCorrectLevel.medium => _rsBlockTable[(typeNumber - 1) * 4 + 1], QrErrorCorrectLevel.M => _rsBlockTable[(typeNumber - 1) * 4 + 1],
QrErrorCorrectLevel.quartile => _rsBlockTable[(typeNumber - 1) * 4 + 2], QrErrorCorrectLevel.Q => _rsBlockTable[(typeNumber - 1) * 4 + 2],
QrErrorCorrectLevel.high => _rsBlockTable[(typeNumber - 1) * 4 + 3], QrErrorCorrectLevel.H => _rsBlockTable[(typeNumber - 1) * 4 + 3],
_ => throw ArgumentError(
'bad rs block @ typeNumber: $typeNumber/errorCorrectLevel:$errorCorrectLevel',
),
}; };
const List<List<int>> _rsBlockTable = [ const List<List<int>> _rsBlockTable = [

View file

@ -12,13 +12,9 @@ dependencies:
meta: ^1.7.0 meta: ^1.7.0
dev_dependencies: dev_dependencies:
args: ^2.1.0
benchmark_harness: ^2.0.0
build_runner: ^2.2.1 build_runner: ^2.2.1
build_web_compilers: ^4.1.4 build_web_compilers: ^4.1.4
dart_flutter_team_lints: ^3.0.0 dart_flutter_team_lints: ^3.0.0
path: ^1.9.1
stream_transform: ^2.0.0 stream_transform: ^2.0.0
test: ^1.21.6 test: ^1.21.6
test_process: ^2.1.1
web: ^1.1.0 web: ^1.1.0

View file

@ -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<int> 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<bool> _getModules(QrImage image) {
final modules = <bool>[];
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
];

View file

@ -1,4 +1,5 @@
import 'package:qr/qr.dart'; import 'package:qr/qr.dart';
import 'package:qr/src/byte.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
@ -6,11 +7,11 @@ void main() {
final qr = QrAlphaNumeric.fromString( final qr = QrAlphaNumeric.fromString(
r'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:', r'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:',
); );
expect(qr.mode, QrMode.alphaNumeric); expect(qr.mode, 2);
expect(qr.length, 45); expect(qr.length, 45);
final buffer = QrBitBuffer(); final buffer = QrBitBuffer();
qr.write(buffer); qr.write(buffer);
expect(buffer, hasLength(248)); expect(buffer.length, 248);
expect( expect(
buffer.map<String>((e) => e ? '1' : '0').join(), buffer.map<String>((e) => e ? '1' : '0').join(),
'00000000001' '00000000001'
@ -41,21 +42,21 @@ void main() {
test('single alphanumeric', () { test('single alphanumeric', () {
final qr = QrAlphaNumeric.fromString(r'$'); final qr = QrAlphaNumeric.fromString(r'$');
expect(qr.mode, QrMode.alphaNumeric); expect(qr.mode, 2);
expect(qr.length, 1); expect(qr.length, 1);
final buffer = QrBitBuffer(); final buffer = QrBitBuffer();
qr.write(buffer); qr.write(buffer);
expect(buffer, hasLength(6)); expect(buffer.length, 6);
expect(buffer.map<String>((e) => e ? '1' : '0').join(), '100101'); expect(buffer.map<String>((e) => e ? '1' : '0').join(), '100101');
}); });
test('double (even) alphanumeric', () { test('double (even) alphanumeric', () {
final qr = QrAlphaNumeric.fromString('3Z'); final qr = QrAlphaNumeric.fromString('3Z');
expect(qr.mode, QrMode.alphaNumeric); expect(qr.mode, 2);
expect(qr.length, 2); expect(qr.length, 2);
final buffer = QrBitBuffer(); final buffer = QrBitBuffer();
qr.write(buffer); qr.write(buffer);
expect(buffer, hasLength(11), reason: 'n*5+1 = 11'); expect(buffer.length, 11, reason: 'n*5+1 = 11');
expect( expect(
buffer buffer
.getRange(0, 11) .getRange(0, 11)

View file

@ -1,6 +1,7 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:qr/qr.dart'; import 'package:qr/qr.dart';
import 'package:qr/src/byte.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {

View file

@ -11,32 +11,36 @@ import 'qr_code_test_data_with_mask.dart';
void main() { void main() {
test('simple', () { test('simple', () {
for (var typeNumber = 1; typeNumber <= 40; typeNumber++) { 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 qr = QrImage(QrCode(typeNumber, quality)..addData('shanna!'));
final modules = qr.qrModules; final modules = qr.qrModules;
for (var i = 0; i < modules.length; i++) {
expect( expect(
modules.map(_encodeBoolListToString), _encodeBoolListToString(modules[i]),
qrCodeTestData[typeNumber.toString()][quality.index.toString()], qrCodeTestData[typeNumber.toString()][quality.toString()][i],
); );
} }
} }
}
}); });
test('fromData', () { test('fromData', () {
for (var quality in QrErrorCorrectLevel.values) { for (var quality in QrErrorCorrectLevel.levels) {
final qr = QrImage( final qr = QrImage(
QrCode.fromData(data: 'shanna!', errorCorrectLevel: quality), QrCode.fromData(data: 'shanna!', errorCorrectLevel: quality),
); );
final modules = qr.qrModules; final modules = qr.qrModules;
for (var i = 0; i < modules.length; i++) {
expect( expect(
modules.map(_encodeBoolListToString), _encodeBoolListToString(modules[i]),
qrCodeTestData['1'][quality.index.toString()], qrCodeTestData['1'][quality.toString()][i],
); );
} }
}
}); });
test('fromUint8List', () { test('fromUint8List', () {
for (var quality in QrErrorCorrectLevel.values) { for (var quality in QrErrorCorrectLevel.levels) {
final qr = QrImage( final qr = QrImage(
QrCode.fromUint8List( QrCode.fromUint8List(
data: Uint8List.fromList([115, 104, 97, 110, 110, 97, 33]), data: Uint8List.fromList([115, 104, 97, 110, 110, 97, 33]),
@ -44,53 +48,67 @@ void main() {
), ),
); );
final modules = qr.qrModules; final modules = qr.qrModules;
for (var i = 0; i < modules.length; i++) {
expect( expect(
modules.map(_encodeBoolListToString), _encodeBoolListToString(modules[i]),
qrCodeTestData['1'][quality.index.toString()], qrCodeTestData['1'][quality.toString()][i],
); );
} }
}
}); });
test('WHEN mask pattern is provided, SHOULD make a masked QR Code', () { test('WHEN mask pattern is provided, SHOULD make a masked QR Code', () {
for (var mask = 0; mask <= 7; mask++) { for (var mask = 0; mask <= 7; mask++) {
final qr = QrImage.withMaskPattern( final qr = QrImage.withMaskPattern(
QrCode(1, QrErrorCorrectLevel.low)..addData('shanna!'), QrCode(1, QrErrorCorrectLevel.L)..addData('shanna!'),
mask, mask,
); );
final modules = qr.qrModules; final modules = qr.qrModules;
for (var i = 0; i < modules.length; i++) {
expect( expect(
modules.map(_encodeBoolListToString), _encodeBoolListToString(modules[i]),
qrCodeTestDataWithMask[mask.toString()], qrCodeTestDataWithMask[mask.toString()][i],
); );
} }
}
}); });
test('WHEN provided mask pattern is smaller than 0, ' test(
'SHOULD throw an AssertionError', () { '''
WHEN provided mask pattern is smaller than 0,
SHOULD throw an AssertionError
''',
() {
expect(() { expect(() {
QrImage.withMaskPattern( QrImage.withMaskPattern(
QrCode(1, QrErrorCorrectLevel.low)..addData('shanna!'), QrCode(1, QrErrorCorrectLevel.L)..addData('shanna!'),
-1, -1,
); );
}, throwsA(isA<AssertionError>())); }, throwsA(isA<AssertionError>()));
}); },
);
test('WHEN provided mask pattern is bigger than 7, ' test(
'SHOULD throw an AssertionError', () { '''
WHEN provided mask pattern is bigger than 7,
SHOULD throw an AssertionError
''',
() {
expect(() { expect(() {
QrImage.withMaskPattern( QrImage.withMaskPattern(
QrCode(1, QrErrorCorrectLevel.high)..addData('shanna!'), QrCode(1, QrErrorCorrectLevel.L)..addData('shanna!'),
8, 8,
); );
}, throwsA(isA<AssertionError>())); }, throwsA(isA<AssertionError>()));
}); },
);
group('QrCode.fromData Automatic Mode Detection', () { group('QrCode.fromData Automatic Mode Detection', () {
// Numeric Mode // Numeric Mode
test('should use Numeric Mode for numbers', () { test('should use Numeric Mode for numbers', () {
// 9 numeric chars fit version 1 (H level). // 9 numeric chars fit version 1 (H level).
final qr = QrCode.fromData( final qr = QrCode.fromData(
data: '123456789', data: '123456789',
errorCorrectLevel: QrErrorCorrectLevel.high, errorCorrectLevel: QrErrorCorrectLevel.H,
); );
expect(qr.typeNumber, 1); expect(qr.typeNumber, 1);
}); });
@ -101,7 +119,7 @@ void main() {
// version 2 (H level, 16 chars). // version 2 (H level, 16 chars).
final qr = QrCode.fromData( final qr = QrCode.fromData(
data: 'HELLO WORLD A', data: 'HELLO WORLD A',
errorCorrectLevel: QrErrorCorrectLevel.high, errorCorrectLevel: QrErrorCorrectLevel.H,
); );
expect(qr.typeNumber, 2); expect(qr.typeNumber, 2);
}); });
@ -112,7 +130,7 @@ void main() {
// '機械学習' (12 bytes) fits version 2 (H level, 16 bytes). // '機械学習' (12 bytes) fits version 2 (H level, 16 bytes).
final qr = QrCode.fromData( final qr = QrCode.fromData(
data: '機械学習', data: '機械学習',
errorCorrectLevel: QrErrorCorrectLevel.high, errorCorrectLevel: QrErrorCorrectLevel.H,
); );
expect(qr.typeNumber, 2); expect(qr.typeNumber, 2);
}); });
@ -122,7 +140,7 @@ void main() {
// Numeric Mode // Numeric Mode
test('should use Numeric Mode for numbers', () { test('should use Numeric Mode for numbers', () {
// 9 numeric characters fit version 1 (H level). // 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); expect(qr.typeNumber, 1);
}); });
@ -130,7 +148,7 @@ void main() {
test('should use Alphanumeric Mode', () { test('should use Alphanumeric Mode', () {
// 13 alphanumeric characters exceed version 1 (7 chars) but fit // 13 alphanumeric characters exceed version 1 (7 chars) but fit
// version 2 (H level, 16 chars). // 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); expect(qr.typeNumber, 2);
}); });
@ -138,7 +156,7 @@ void main() {
test('should use Byte Mode for non-alphanumeric characters', () { test('should use Byte Mode for non-alphanumeric characters', () {
// Kanji characters are UTF-8 encoded. // Kanji characters are UTF-8 encoded.
// '機械学習' (12 bytes) fits version 2 (H level, 16 bytes). // '機械学習' (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); expect(qr.typeNumber, 2);
}); });
}); });
@ -150,7 +168,7 @@ void main() {
final qrCode = QrCode.fromData( final qrCode = QrCode.fromData(
data: largeData, data: largeData,
errorCorrectLevel: QrErrorCorrectLevel.low, errorCorrectLevel: QrErrorCorrectLevel.L,
); );
expect(qrCode.typeNumber, 40); expect(qrCode.typeNumber, 40);
@ -164,20 +182,12 @@ void main() {
expect( expect(
() => QrCode.fromData( () => QrCode.fromData(
data: excessivelyLargeData, data: excessivelyLargeData,
errorCorrectLevel: QrErrorCorrectLevel.low, errorCorrectLevel: QrErrorCorrectLevel.L,
), ),
throwsA(isA<InputTooLongException>()), throwsA(isA<InputTooLongException>()),
); );
}); });
}); });
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<InputTooLongException>()));
});
});
} }
String _encodeBoolListToString(List<bool?> source) => String _encodeBoolListToString(List<bool?> source) =>

View file

@ -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<QrNumeric>());
});
test('AlphaNumeric', () {
final datums = QrDatum.toDatums('HELLO WORLD');
expect(datums, hasLength(1));
expect(datums.first, isA<QrAlphaNumeric>());
});
test('Byte (Latin-1)', () {
final datums = QrDatum.toDatums('Hello World!');
expect(datums, hasLength(1));
expect(datums.first, isA<QrByte>());
});
test('Byte (UTF-8 with ECI)', () {
final datums = QrDatum.toDatums('Hello 🌍');
expect(datums, hasLength(2));
expect(datums[0], isA<QrEci>());
expect((datums[0] as QrEci).value, 26);
expect(datums[1], isA<QrByte>());
});
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<QrEci>());
expect((datums[0] as QrEci).value, 26);
expect(datums[1], isA<QrByte>());
});
});
}

View file

@ -1,14 +1,15 @@
import 'package:qr/qr.dart'; import 'package:qr/qr.dart';
import 'package:qr/src/byte.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
test('all digits 1 through 0', () { test('all digits 1 through 0', () {
final qr = QrNumeric.fromString('1234567890'); final qr = QrNumeric.fromString('1234567890');
expect(qr.mode, QrMode.numeric); expect(qr.mode, 1);
expect(qr.length, 10); expect(qr.length, 10);
final buffer = QrBitBuffer(); final buffer = QrBitBuffer();
qr.write(buffer); qr.write(buffer);
expect(buffer, hasLength(34)); expect(buffer.length, 34);
expect( expect(
buffer buffer
.getRange(0, 10) .getRange(0, 10)
@ -53,11 +54,11 @@ void main() {
test('single numeric', () { test('single numeric', () {
final qr = QrNumeric.fromString('5'); final qr = QrNumeric.fromString('5');
expect(qr.mode, QrMode.numeric); expect(qr.mode, 1);
expect(qr.length, 1); expect(qr.length, 1);
final buffer = QrBitBuffer(); final buffer = QrBitBuffer();
qr.write(buffer); qr.write(buffer);
expect(buffer, hasLength(4)); expect(buffer.length, 4);
expect( expect(
buffer buffer
.getRange(0, 4) .getRange(0, 4)
@ -72,11 +73,11 @@ void main() {
test('double numeric', () { test('double numeric', () {
final qr = QrNumeric.fromString('37'); final qr = QrNumeric.fromString('37');
expect(qr.mode, QrMode.numeric); expect(qr.mode, 1);
expect(qr.length, 2); expect(qr.length, 2);
final buffer = QrBitBuffer(); final buffer = QrBitBuffer();
qr.write(buffer); qr.write(buffer);
expect(buffer, hasLength(7), reason: 'n*3+1 = 7'); expect(buffer.length, 7, reason: 'n*3+1 = 7');
expect( expect(
buffer buffer
.getRange(0, 7) .getRange(0, 7)
@ -91,11 +92,11 @@ void main() {
test('triple (even) numeric', () { test('triple (even) numeric', () {
final qr = QrNumeric.fromString('371'); final qr = QrNumeric.fromString('371');
expect(qr.mode, QrMode.numeric); expect(qr.mode, 1);
expect(qr.length, 3); expect(qr.length, 3);
final buffer = QrBitBuffer(); final buffer = QrBitBuffer();
qr.write(buffer); qr.write(buffer);
expect(buffer, hasLength(10), reason: 'n*3+1 = 10'); expect(buffer.length, 10, reason: 'n*3+1 = 10');
expect( expect(
buffer buffer
.getRange(0, 10) .getRange(0, 10)

View file

@ -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));
});
}

View file

@ -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);
});
}