twonly-app-dependencies/qr/lib/src/qr_image.dart
2026-02-12 22:01:59 +01:00

504 lines
14 KiB
Dart

import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'error_correct_level.dart';
import 'mask_pattern.dart';
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 maskPattern;
final Uint8List _data;
/// 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;
// 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 lostPoint = _lostPoint(testImage);
if (lostPoint < minLostPoint) {
minLostPoint = lostPoint;
bestMaskPattern = i;
// Copy working buffer to bestData
bestData ??= Uint8List(dataSize);
bestData.setRange(0, dataSize, workingBuffer);
}
}
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;
}
/// Generates a specific image for the [qrCode] and [maskPattern].
QrImage.withMaskPattern(QrCode qrCode, this.maskPattern)
: assert(maskPattern >= 0 && maskPattern <= 7),
moduleCount = qrCode.moduleCount,
typeNumber = qrCode.typeNumber,
errorCorrectLevel = qrCode.errorCorrectLevel,
_data = Uint8List(qrCode.moduleCount * qrCode.moduleCount) {
_makeImpl(maskPattern, qrCode.dataCache, false);
}
/// Internal constructor for template creation
QrImage._template(QrCode qrCode)
: 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);
}
}
/// Internal constructor for testing phase
QrImage._fromData(QrCode qrCode, this.maskPattern, this._data)
: moduleCount = qrCode.moduleCount,
typeNumber = qrCode.typeNumber,
errorCorrectLevel = qrCode.errorCorrectLevel;
@visibleForTesting
List<List<bool?>> get qrModules {
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() {
_data.fillRange(0, _data.length, _pixelUnassigned);
}
bool isDark(int row, int col) {
if (row < 0 || moduleCount <= row) {
throw RangeError.range(row, 0, moduleCount - 1, 'row');
}
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;
}
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();
_setupPositionProbePattern(0, 0);
_setupPositionProbePattern(moduleCount - 7, 0);
_setupPositionProbePattern(0, moduleCount - 7);
_setupPositionAdjustPattern();
_setupTimingPattern();
_setupTypeInfo(maskPattern, test);
if (typeNumber >= 7) {
_setupTypeNumber(test);
}
_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;
for (var c = -1; c <= 7; c++) {
if (col + c <= -1 || moduleCount <= col + c) continue;
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);
} else {
_set(row + r, col + c, false);
}
}
}
}
void _setupPositionAdjustPattern() {
final pos = qr_util.patternPosition(typeNumber);
for (var i = 0; i < pos.length; i++) {
for (var j = 0; j < pos.length; j++) {
final row = pos[i];
final col = pos[j];
if (_data[row * moduleCount + col] != _pixelUnassigned) {
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);
} else {
_set(row + r, col + c, false);
}
}
}
}
}
}
void _setupTimingPattern() {
for (var r = 8; r < moduleCount - 8; r++) {
if (_data[r * moduleCount + 6] != _pixelUnassigned) {
continue;
}
_set(r, 6, r.isEven);
}
for (var c = 8; c < moduleCount - 8; c++) {
if (_data[6 * moduleCount + c] != _pixelUnassigned) {
continue;
}
_set(6, c, c.isEven);
}
}
void _setupTypeInfo(int maskPattern, bool test) {
final data = (errorCorrectLevel.index << 3) | maskPattern;
final bits = qr_util.bchTypeInfo(data);
int i;
bool mod;
// vertical
for (i = 0; i < 15; i++) {
mod = !test && ((bits >> i) & 1) == 1;
if (i < 6) {
_set(i, 8, mod);
} else if (i < 8) {
_set(i + 1, 8, mod);
} else {
_set(moduleCount - 15 + i, 8, mod);
}
}
// horizontal
for (i = 0; i < 15; i++) {
mod = !test && ((bits >> i) & 1) == 1;
if (i < 8) {
_set(8, moduleCount - i - 1, mod);
} else if (i < 9) {
_set(8, 15 - i - 1 + 1, mod);
} else {
_set(8, 15 - i - 1, mod);
}
}
// fixed module
_set(moduleCount - 8, 8, !test);
}
void _setupTypeNumber(bool test) {
final bits = qr_util.bchTypeNumber(typeNumber);
for (var i = 0; i < 18; i++) {
final mod = !test && ((bits >> i) & 1) == 1;
_set(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);
}
}
void _mapData(List<int> data, int maskPattern) {
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;
}
final mask = QrMaskPattern.values[maskPattern].check(row, col - c);
if (mask) {
dark = !dark;
}
_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 _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;
}
}
}
}
}
double _lostPoint(QrImage qrImage) {
final moduleCount = qrImage.moduleCount;
final data = qrImage._data;
var lostPoint = 0.0;
// 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++) {
var sameCount = 0;
final currentIdx = row * moduleCount + col;
final isDark = data[currentIdx] == QrImage._pixelDark;
// Check all 8 neighbors
// Top row
if (row > 0) {
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)
if (col > 0 && (data[currentIdx - 1] == QrImage._pixelDark) == isDark) {
sameCount++;
}
if (col < moduleCount - 1 &&
(data[currentIdx + 1] == QrImage._pixelDark) == isDark) {
sameCount++;
}
// 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 (sameCount > 5) {
lostPoint += 3 + sameCount - 5;
}
}
}
// 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) {
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) {
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) {
lostPoint += 40;
}
}
}
// Level 4: Dark ratio
var darkCount = 0;
for (var i = 0; i < data.length; i++) {
if (data[i] == QrImage._pixelDark) darkCount++;
}
final ratio = (100 * darkCount / moduleCount / moduleCount - 50).abs() / 5;
return lostPoint + ratio * 10;
}