add new dependency
|
|
@ -11,4 +11,5 @@ optional: 71c638891ce4f2aff35c7387727989f31f9d877d
|
||||||
photo_view: a13ca2fc387a3fb1276126959e092c44d0029987
|
photo_view: a13ca2fc387a3fb1276126959e092c44d0029987
|
||||||
pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
|
pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
|
||||||
qr: ff808bb3f354e6a7029ec953cbe0144a42021db6
|
qr: ff808bb3f354e6a7029ec953cbe0144a42021db6
|
||||||
|
qr_flutter: d5e7206396105d643113618290bbcc755d05f492
|
||||||
x25519: ecb1d357714537bba6e276ef45f093846d4beaee
|
x25519: ecb1d357714537bba6e276ef45f093846d4beaee
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
qr_flutter:
|
||||||
|
git: https://github.com/theyakka/qr.flutter.git
|
||||||
|
dependencies:
|
||||||
qr:
|
qr:
|
||||||
git: https://github.com/kevmoo/qr.dart.git
|
git: https://github.com/kevmoo/qr.dart.git
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,5 +25,7 @@ dependency_overrides:
|
||||||
path: ./dependencies/pointycastle
|
path: ./dependencies/pointycastle
|
||||||
qr:
|
qr:
|
||||||
path: ./dependencies/qr
|
path: ./dependencies/qr
|
||||||
|
qr_flutter:
|
||||||
|
path: ./dependencies/qr_flutter
|
||||||
x25519:
|
x25519:
|
||||||
path: ./dependencies/x25519
|
path: ./dependencies/x25519
|
||||||
|
|
|
||||||
29
qr_flutter/LICENSE
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
BSD 3-Clause License
|
||||||
|
|
||||||
|
Copyright (c) 2020, Luke Freeman.
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
14
qr_flutter/lib/qr_flutter.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* QR.Flutter
|
||||||
|
* Copyright (c) 2019 the QR.Flutter authors.
|
||||||
|
* See LICENSE for distribution and usage details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export 'package:qr/qr.dart';
|
||||||
|
|
||||||
|
export 'src/errors.dart';
|
||||||
|
export 'src/qr_image_view.dart';
|
||||||
|
export 'src/qr_painter.dart';
|
||||||
|
export 'src/qr_versions.dart';
|
||||||
|
export 'src/types.dart';
|
||||||
|
export 'src/validator.dart';
|
||||||
48
qr_flutter/lib/src/errors.dart
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* QR.Flutter
|
||||||
|
* Copyright (c) 2019 the QR.Flutter authors.
|
||||||
|
* See LICENSE for distribution and usage details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'qr_versions.dart';
|
||||||
|
|
||||||
|
/// An exception that is thrown when an invalid QR code version / type is
|
||||||
|
/// requested.
|
||||||
|
class QrUnsupportedVersionException implements Exception {
|
||||||
|
/// Create a new QrUnsupportedVersionException.
|
||||||
|
factory QrUnsupportedVersionException(int providedVersion) {
|
||||||
|
final message =
|
||||||
|
'Invalid version. $providedVersion is not >= ${QrVersions.min} '
|
||||||
|
'and <= ${QrVersions.max}';
|
||||||
|
return QrUnsupportedVersionException._internal(providedVersion, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
QrUnsupportedVersionException._internal(this.providedVersion, this.message);
|
||||||
|
|
||||||
|
/// The version you passed to the QR code operation.
|
||||||
|
final int providedVersion;
|
||||||
|
|
||||||
|
/// A message describing the exception state.
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'QrUnsupportedVersionException: $message';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An exception that is thrown when something goes wrong with the
|
||||||
|
/// [ImageProvider] for the embedded image of a QrImageView or QrPainter.
|
||||||
|
class QrEmbeddedImageException implements Exception {
|
||||||
|
/// Create a new QrEmbeddedImageException.
|
||||||
|
factory QrEmbeddedImageException(String message) {
|
||||||
|
return QrEmbeddedImageException._internal(message);
|
||||||
|
}
|
||||||
|
QrEmbeddedImageException._internal(this.message);
|
||||||
|
|
||||||
|
/// A message describing the exception state.
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'QrEmbeddedImageException: $message';
|
||||||
|
}
|
||||||
55
qr_flutter/lib/src/paint_cache.dart
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* QR.Flutter
|
||||||
|
* Copyright (c) 2019 the QR.Flutter authors.
|
||||||
|
* See LICENSE for distribution and usage details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
import 'types.dart';
|
||||||
|
|
||||||
|
/// Caches painter objects so we do have to recreate them and waste expensive
|
||||||
|
/// cycles.
|
||||||
|
class PaintCache {
|
||||||
|
final List<Paint> _pixelPaints = <Paint>[];
|
||||||
|
final Map<String, Paint> _keyedPaints = <String, Paint>{};
|
||||||
|
|
||||||
|
String _cacheKey(QrCodeElement element, {FinderPatternPosition? position}) {
|
||||||
|
final posKey = position != null ? position.toString() : 'any';
|
||||||
|
return '$element:$posKey';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save a [Paint] for the provided element and position into the cache.
|
||||||
|
void cache(
|
||||||
|
Paint paint,
|
||||||
|
QrCodeElement element, {
|
||||||
|
FinderPatternPosition? position,
|
||||||
|
}) {
|
||||||
|
if (element == QrCodeElement.codePixel) {
|
||||||
|
_pixelPaints.add(paint);
|
||||||
|
} else {
|
||||||
|
_keyedPaints[_cacheKey(element, position: position)] = paint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve the first [Paint] object from the paint cache for the provided
|
||||||
|
/// element and position.
|
||||||
|
Paint? firstPaint(QrCodeElement element, {FinderPatternPosition? position}) {
|
||||||
|
return element == QrCodeElement.codePixel
|
||||||
|
? _pixelPaints.first
|
||||||
|
: _keyedPaints[_cacheKey(element, position: position)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieve all [Paint] objects from the paint cache for the provided
|
||||||
|
/// element and position. Note: Finder pattern elements can only have a max
|
||||||
|
/// one [Paint] object per position. As such they will always return a [List]
|
||||||
|
/// with a fixed size of `1`.
|
||||||
|
List<Paint?> paints(
|
||||||
|
QrCodeElement element, {
|
||||||
|
FinderPatternPosition? position,
|
||||||
|
}) {
|
||||||
|
return element == QrCodeElement.codePixel
|
||||||
|
? _pixelPaints
|
||||||
|
: <Paint?>[_keyedPaints[_cacheKey(element, position: position)]];
|
||||||
|
}
|
||||||
|
}
|
||||||
336
qr_flutter/lib/src/qr_image_view.dart
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
/*
|
||||||
|
* QR.Flutter
|
||||||
|
* Copyright (c) 2019 the QR.Flutter authors.
|
||||||
|
* See LICENSE for distribution and usage details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:qr/qr.dart';
|
||||||
|
|
||||||
|
import 'qr_painter.dart';
|
||||||
|
import 'qr_versions.dart';
|
||||||
|
import 'types.dart';
|
||||||
|
import 'validator.dart';
|
||||||
|
|
||||||
|
/// A widget that shows a QR code.
|
||||||
|
class QrImageView extends StatefulWidget {
|
||||||
|
/// Create a new QR code using the [String] data and the passed options (or
|
||||||
|
/// using the default options).
|
||||||
|
QrImageView({
|
||||||
|
required String data,
|
||||||
|
super.key,
|
||||||
|
this.size,
|
||||||
|
this.padding = const EdgeInsets.all(10.0),
|
||||||
|
this.backgroundColor = Colors.transparent,
|
||||||
|
@Deprecated('use colors in eyeStyle and dataModuleStyle instead')
|
||||||
|
this.foregroundColor = Colors.black,
|
||||||
|
this.version = QrVersions.auto,
|
||||||
|
this.errorCorrectionLevel = QrErrorCorrectLevel.L,
|
||||||
|
this.errorStateBuilder,
|
||||||
|
this.constrainErrorBounds = true,
|
||||||
|
this.gapless = true,
|
||||||
|
this.embeddedImage,
|
||||||
|
this.embeddedImageStyle = const QrEmbeddedImageStyle(),
|
||||||
|
this.semanticsLabel = 'qr code',
|
||||||
|
this.eyeStyle = const QrEyeStyle(
|
||||||
|
eyeShape: QrEyeShape.square,
|
||||||
|
),
|
||||||
|
this.dataModuleStyle = const QrDataModuleStyle(
|
||||||
|
dataModuleShape: QrDataModuleShape.square,
|
||||||
|
),
|
||||||
|
this.embeddedImageEmitsError = false,
|
||||||
|
this.gradient,
|
||||||
|
}) : assert(
|
||||||
|
QrVersions.isSupportedVersion(version),
|
||||||
|
'QR code version $version is not supported',
|
||||||
|
),
|
||||||
|
_data = data,
|
||||||
|
_qrCode = null;
|
||||||
|
|
||||||
|
/// Create a new QR code using the [QrCode] data and the passed options (or
|
||||||
|
/// using the default options).
|
||||||
|
QrImageView.withQr({
|
||||||
|
required QrCode qr,
|
||||||
|
super.key,
|
||||||
|
this.size,
|
||||||
|
this.padding = const EdgeInsets.all(10.0),
|
||||||
|
this.backgroundColor = Colors.transparent,
|
||||||
|
@Deprecated('use colors in eyeStyle and dataModuleStyle instead')
|
||||||
|
this.foregroundColor = Colors.black,
|
||||||
|
this.version = QrVersions.auto,
|
||||||
|
this.errorCorrectionLevel = QrErrorCorrectLevel.L,
|
||||||
|
this.errorStateBuilder,
|
||||||
|
this.constrainErrorBounds = true,
|
||||||
|
this.gapless = true,
|
||||||
|
this.embeddedImage,
|
||||||
|
this.embeddedImageStyle = const QrEmbeddedImageStyle(),
|
||||||
|
this.semanticsLabel = 'qr code',
|
||||||
|
this.eyeStyle = const QrEyeStyle(
|
||||||
|
eyeShape: QrEyeShape.square,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
this.dataModuleStyle = const QrDataModuleStyle(
|
||||||
|
dataModuleShape: QrDataModuleShape.square,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
this.embeddedImageEmitsError = false,
|
||||||
|
this.gradient,
|
||||||
|
}) : assert(
|
||||||
|
QrVersions.isSupportedVersion(version),
|
||||||
|
'QR code version $version is not supported',
|
||||||
|
),
|
||||||
|
_data = null,
|
||||||
|
_qrCode = qr;
|
||||||
|
|
||||||
|
// The data passed to the widget
|
||||||
|
final String? _data;
|
||||||
|
|
||||||
|
// The QR code data passed to the widget
|
||||||
|
final QrCode? _qrCode;
|
||||||
|
|
||||||
|
/// The background color of the final QR code widget.
|
||||||
|
final Color backgroundColor;
|
||||||
|
|
||||||
|
/// The foreground color of the final QR code widget.
|
||||||
|
@Deprecated('use colors in eyeStyle and dataModuleStyle instead')
|
||||||
|
final Color foregroundColor;
|
||||||
|
|
||||||
|
/// The gradient for all (dataModule and eye)
|
||||||
|
final Gradient? gradient;
|
||||||
|
|
||||||
|
/// The QR code version to use.
|
||||||
|
final int version;
|
||||||
|
|
||||||
|
/// The QR code error correction level to use.
|
||||||
|
final int errorCorrectionLevel;
|
||||||
|
|
||||||
|
/// The external padding between the edge of the widget and the content.
|
||||||
|
final EdgeInsets padding;
|
||||||
|
|
||||||
|
/// The intended size of the widget.
|
||||||
|
final double? size;
|
||||||
|
|
||||||
|
/// The callback that is executed in the event of an error so that you can
|
||||||
|
/// interrogate the exception and construct an alternative view to present
|
||||||
|
/// to your user.
|
||||||
|
final QrErrorBuilder? errorStateBuilder;
|
||||||
|
|
||||||
|
/// If `true` then the error widget will be constrained to the boundary of the
|
||||||
|
/// QR widget if it had been valid. If `false` the error widget will grow to
|
||||||
|
/// the size it needs. If the error widget is allowed to grow, your layout may
|
||||||
|
/// jump around (depending on specifics).
|
||||||
|
///
|
||||||
|
/// NOTE: Setting a [size] value will override this setting and both the
|
||||||
|
/// content widget and error widget will adhere to the size value.
|
||||||
|
final bool constrainErrorBounds;
|
||||||
|
|
||||||
|
/// If set to false, each of the squares in the QR code will have a small
|
||||||
|
/// gap. Default is true.
|
||||||
|
final bool gapless;
|
||||||
|
|
||||||
|
/// The image data to embed (as an overlay) in the QR code. The image will
|
||||||
|
/// be added to the center of the QR code.
|
||||||
|
final ImageProvider? embeddedImage;
|
||||||
|
|
||||||
|
/// Styling options for the image overlay.
|
||||||
|
final QrEmbeddedImageStyle embeddedImageStyle;
|
||||||
|
|
||||||
|
/// If set to true and there is an error loading the embedded image, the
|
||||||
|
/// [errorStateBuilder] callback will be called (if it is defined). If false,
|
||||||
|
/// the widget will ignore the embedded image and just display the QR code.
|
||||||
|
/// The default is false.
|
||||||
|
final bool embeddedImageEmitsError;
|
||||||
|
|
||||||
|
/// [semanticsLabel] will be used by screen readers to describe the content of
|
||||||
|
/// the qr code.
|
||||||
|
/// Default is 'qr code'.
|
||||||
|
final String semanticsLabel;
|
||||||
|
|
||||||
|
/// Styling option for QR Eye ball and frame.
|
||||||
|
final QrEyeStyle eyeStyle;
|
||||||
|
|
||||||
|
/// Styling option for QR data module.
|
||||||
|
final QrDataModuleStyle dataModuleStyle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<QrImageView> createState() => _QrImageViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QrImageViewState extends State<QrImageView> {
|
||||||
|
/// The QR code string data.
|
||||||
|
QrCode? _qr;
|
||||||
|
|
||||||
|
/// The current validation status.
|
||||||
|
late QrValidationResult _validationResult;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget._data != null) {
|
||||||
|
_validationResult = QrValidator.validate(
|
||||||
|
data: widget._data!,
|
||||||
|
version: widget.version,
|
||||||
|
errorCorrectionLevel: widget.errorCorrectionLevel,
|
||||||
|
);
|
||||||
|
_qr = _validationResult.isValid ? _validationResult.qrCode : null;
|
||||||
|
} else if (widget._qrCode != null) {
|
||||||
|
_qr = widget._qrCode;
|
||||||
|
_validationResult =
|
||||||
|
QrValidationResult(status: QrValidationStatus.valid, qrCode: _qr);
|
||||||
|
}
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
// validation failed, show an error state widget if builder is present.
|
||||||
|
if (!_validationResult.isValid) {
|
||||||
|
return _errorWidget(context, constraints, _validationResult.error);
|
||||||
|
}
|
||||||
|
// no error, build the regular widget
|
||||||
|
final widgetSize =
|
||||||
|
widget.size ?? constraints.biggest.shortestSide;
|
||||||
|
if (widget.embeddedImage != null) {
|
||||||
|
// if requesting to embed an image then we need to load via a
|
||||||
|
// FutureBuilder because the image provider will be async.
|
||||||
|
return FutureBuilder<ui.Image>(
|
||||||
|
future: _loadQrImage(context, widget.embeddedImageStyle),
|
||||||
|
builder: (ctx, snapshot) {
|
||||||
|
if (snapshot.error != null) {
|
||||||
|
debugPrint('snapshot error: ${snapshot.error}');
|
||||||
|
return widget.embeddedImageEmitsError
|
||||||
|
? _errorWidget(context, constraints, snapshot.error)
|
||||||
|
: _qrWidget(null, widgetSize);
|
||||||
|
}
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
debugPrint('loaded image');
|
||||||
|
final loadedImage = snapshot.data;
|
||||||
|
return _qrWidget(loadedImage, widgetSize);
|
||||||
|
} else {
|
||||||
|
return Container();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return _qrWidget(null, widgetSize);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _qrWidget(ui.Image? image, double edgeLength) {
|
||||||
|
final painter = QrPainter.withQr(
|
||||||
|
qr: _qr!,
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
|
color: widget.foregroundColor,
|
||||||
|
gapless: widget.gapless,
|
||||||
|
embeddedImageStyle: widget.embeddedImageStyle,
|
||||||
|
embeddedImage: image,
|
||||||
|
eyeStyle: widget.eyeStyle,
|
||||||
|
dataModuleStyle: widget.dataModuleStyle,
|
||||||
|
gradient: widget.gradient
|
||||||
|
);
|
||||||
|
return _QrContentView(
|
||||||
|
edgeLength: edgeLength,
|
||||||
|
backgroundColor: widget.backgroundColor,
|
||||||
|
padding: widget.padding,
|
||||||
|
semanticsLabel: widget.semanticsLabel,
|
||||||
|
child: CustomPaint(painter: painter),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _errorWidget(
|
||||||
|
BuildContext context,
|
||||||
|
BoxConstraints constraints,
|
||||||
|
Object? error,
|
||||||
|
) {
|
||||||
|
final errorWidget = widget.errorStateBuilder == null
|
||||||
|
? Container()
|
||||||
|
: widget.errorStateBuilder!(context, error);
|
||||||
|
final errorSideLength = widget.constrainErrorBounds
|
||||||
|
? widget.size ?? constraints.biggest.shortestSide
|
||||||
|
: constraints.biggest.longestSide;
|
||||||
|
return _QrContentView(
|
||||||
|
edgeLength: errorSideLength,
|
||||||
|
backgroundColor: widget.backgroundColor,
|
||||||
|
padding: widget.padding,
|
||||||
|
semanticsLabel: widget.semanticsLabel,
|
||||||
|
child: errorWidget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
late ImageStreamListener streamListener;
|
||||||
|
|
||||||
|
Future<ui.Image> _loadQrImage(
|
||||||
|
BuildContext buildContext,
|
||||||
|
QrEmbeddedImageStyle? style,
|
||||||
|
) {
|
||||||
|
if (style != null) {}
|
||||||
|
|
||||||
|
final mq = MediaQuery.of(buildContext);
|
||||||
|
final completer = Completer<ui.Image>();
|
||||||
|
final stream = widget.embeddedImage!.resolve(
|
||||||
|
ImageConfiguration(
|
||||||
|
devicePixelRatio: mq.devicePixelRatio,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
streamListener = ImageStreamListener(
|
||||||
|
(info, err) {
|
||||||
|
stream.removeListener(streamListener);
|
||||||
|
completer.complete(info.image);
|
||||||
|
},
|
||||||
|
onError: (err, _) {
|
||||||
|
stream.removeListener(streamListener);
|
||||||
|
completer.completeError(err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
stream.addListener(streamListener);
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A function type to be called when any form of error occurs while
|
||||||
|
/// painting a [QrImageView].
|
||||||
|
typedef QrErrorBuilder = Widget Function(BuildContext context, Object? error);
|
||||||
|
|
||||||
|
class _QrContentView extends StatelessWidget {
|
||||||
|
const _QrContentView({
|
||||||
|
required this.edgeLength,
|
||||||
|
required this.child,
|
||||||
|
this.backgroundColor,
|
||||||
|
this.padding,
|
||||||
|
this.semanticsLabel,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The length of both edges (because it has to be a square).
|
||||||
|
final double edgeLength;
|
||||||
|
|
||||||
|
/// The background color of the containing widget.
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
|
/// The padding that surrounds the child widget.
|
||||||
|
final EdgeInsets? padding;
|
||||||
|
|
||||||
|
/// The child widget.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// [semanticsLabel] will be used by screen readers to describe the content of
|
||||||
|
/// the qr code.
|
||||||
|
final String? semanticsLabel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Semantics(
|
||||||
|
label: semanticsLabel,
|
||||||
|
child: Container(
|
||||||
|
width: edgeLength,
|
||||||
|
height: edgeLength,
|
||||||
|
color: backgroundColor,
|
||||||
|
child: Padding(
|
||||||
|
padding: padding!,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
694
qr_flutter/lib/src/qr_painter.dart
Normal file
|
|
@ -0,0 +1,694 @@
|
||||||
|
/*
|
||||||
|
* QR.Flutter
|
||||||
|
* Copyright (c) 2019 the QR.Flutter authors.
|
||||||
|
* See LICENSE for distribution and usage details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:qr/qr.dart';
|
||||||
|
|
||||||
|
import 'errors.dart';
|
||||||
|
import 'paint_cache.dart';
|
||||||
|
import 'qr_versions.dart';
|
||||||
|
import 'types.dart';
|
||||||
|
import 'validator.dart';
|
||||||
|
|
||||||
|
// ignore_for_file: deprecated_member_use_from_same_package
|
||||||
|
|
||||||
|
const int _finderPatternLimit = 7;
|
||||||
|
|
||||||
|
// default colors for the qr code pixels
|
||||||
|
const Color _qrDefaultColor = Color(0xff000000);
|
||||||
|
const Color _qrDefaultEmptyColor = Color(0x00ffffff);
|
||||||
|
|
||||||
|
/// A [CustomPainter] object that you can use to paint a QR code.
|
||||||
|
class QrPainter extends CustomPainter {
|
||||||
|
/// Create a new QRPainter with passed options (or defaults).
|
||||||
|
QrPainter({
|
||||||
|
required String data,
|
||||||
|
required this.version,
|
||||||
|
this.errorCorrectionLevel = QrErrorCorrectLevel.L,
|
||||||
|
@Deprecated('use colors in eyeStyle and dataModuleStyle instead')
|
||||||
|
this.color = _qrDefaultColor,
|
||||||
|
@Deprecated(
|
||||||
|
'You should use the background color value of your container widget',
|
||||||
|
)
|
||||||
|
this.emptyColor = _qrDefaultEmptyColor,
|
||||||
|
this.gapless = false,
|
||||||
|
this.embeddedImage,
|
||||||
|
this.embeddedImageStyle = const QrEmbeddedImageStyle(),
|
||||||
|
this.eyeStyle = const QrEyeStyle(),
|
||||||
|
this.dataModuleStyle = const QrDataModuleStyle(),
|
||||||
|
this.gradient,
|
||||||
|
}) : assert(
|
||||||
|
QrVersions.isSupportedVersion(version),
|
||||||
|
'QR code version $version is not supported',
|
||||||
|
) {
|
||||||
|
_init(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new QrPainter with a pre-validated/created [QrCode] object. This
|
||||||
|
/// constructor is useful when you have a custom validation / error handling
|
||||||
|
/// flow or for when you need to pre-validate the QR data.
|
||||||
|
QrPainter.withQr({
|
||||||
|
required QrCode qr,
|
||||||
|
@Deprecated('use colors in eyeStyle and dataModuleStyle instead')
|
||||||
|
this.color = _qrDefaultColor,
|
||||||
|
@Deprecated(
|
||||||
|
'You should use the background color value of your container widget',
|
||||||
|
)
|
||||||
|
this.emptyColor = _qrDefaultEmptyColor,
|
||||||
|
this.gapless = false,
|
||||||
|
this.embeddedImage,
|
||||||
|
this.embeddedImageStyle = const QrEmbeddedImageStyle(),
|
||||||
|
this.eyeStyle = const QrEyeStyle(),
|
||||||
|
this.dataModuleStyle = const QrDataModuleStyle(),
|
||||||
|
this.gradient,
|
||||||
|
}) : _qr = qr,
|
||||||
|
version = qr.typeNumber,
|
||||||
|
errorCorrectionLevel = qr.errorCorrectLevel {
|
||||||
|
_calcVersion = version;
|
||||||
|
_initPaints();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The QR code version.
|
||||||
|
final int version; // the qr code version
|
||||||
|
|
||||||
|
/// The error correction level of the QR code.
|
||||||
|
final int errorCorrectionLevel; // the qr code error correction level
|
||||||
|
|
||||||
|
/// The color of the squares.
|
||||||
|
@Deprecated('use colors in eyeStyle and dataModuleStyle instead')
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
/// The gradient for all (dataModuleShape, eyeShape, embeddedImageShape)
|
||||||
|
final Gradient? gradient;
|
||||||
|
|
||||||
|
/// The color of the non-squares (background).
|
||||||
|
@Deprecated(
|
||||||
|
'You should use the background color value of your container widget')
|
||||||
|
final Color emptyColor; // the other color
|
||||||
|
/// If set to false, the painter will leave a 1px gap between each of the
|
||||||
|
/// squares.
|
||||||
|
final bool gapless;
|
||||||
|
|
||||||
|
/// The image data to embed (as an overlay) in the QR code. The image will
|
||||||
|
/// be added to the center of the QR code.
|
||||||
|
final ui.Image? embeddedImage;
|
||||||
|
|
||||||
|
/// Styling options for the image overlay.
|
||||||
|
final QrEmbeddedImageStyle embeddedImageStyle;
|
||||||
|
|
||||||
|
/// Styling option for QR Eye ball and frame.
|
||||||
|
final QrEyeStyle eyeStyle;
|
||||||
|
|
||||||
|
/// Styling option for QR data module.
|
||||||
|
final QrDataModuleStyle dataModuleStyle;
|
||||||
|
|
||||||
|
/// The base QR code data
|
||||||
|
QrCode? _qr;
|
||||||
|
|
||||||
|
/// QR Image renderer
|
||||||
|
late QrImage _qrImage;
|
||||||
|
|
||||||
|
/// This is the version (after calculating) that we will use if the user has
|
||||||
|
/// requested the 'auto' version.
|
||||||
|
late final int _calcVersion;
|
||||||
|
|
||||||
|
/// The size of the 'gap' between the pixels
|
||||||
|
final double _gapSize = 0.25;
|
||||||
|
|
||||||
|
/// Cache for all of the [Paint] objects.
|
||||||
|
final PaintCache _paintCache = PaintCache();
|
||||||
|
|
||||||
|
void _init(String data) {
|
||||||
|
if (!QrVersions.isSupportedVersion(version)) {
|
||||||
|
throw QrUnsupportedVersionException(version);
|
||||||
|
}
|
||||||
|
// configure and make the QR code data
|
||||||
|
final validationResult = QrValidator.validate(
|
||||||
|
data: data,
|
||||||
|
version: version,
|
||||||
|
errorCorrectionLevel: errorCorrectionLevel,
|
||||||
|
);
|
||||||
|
if (!validationResult.isValid) {
|
||||||
|
throw validationResult.error!;
|
||||||
|
}
|
||||||
|
_qr = validationResult.qrCode;
|
||||||
|
_calcVersion = _qr!.typeNumber;
|
||||||
|
_initPaints();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initPaints() {
|
||||||
|
// Initialize `QrImage` for rendering
|
||||||
|
_qrImage = QrImage(_qr!);
|
||||||
|
// Cache the pixel paint object. For now there is only one but we might
|
||||||
|
// expand it to multiple later (e.g.: different colours).
|
||||||
|
_paintCache.cache(
|
||||||
|
Paint()..style = PaintingStyle.fill,
|
||||||
|
QrCodeElement.codePixel,
|
||||||
|
);
|
||||||
|
// Cache the empty pixel paint object. Empty color is deprecated and will go
|
||||||
|
// away.
|
||||||
|
_paintCache.cache(
|
||||||
|
Paint()..style = PaintingStyle.fill,
|
||||||
|
QrCodeElement.codePixelEmpty,
|
||||||
|
);
|
||||||
|
// Cache the finder pattern painters. We'll keep one for each one in case
|
||||||
|
// we want to provide customization options later.
|
||||||
|
for (final position in FinderPatternPosition.values) {
|
||||||
|
_paintCache.cache(
|
||||||
|
Paint()..style = PaintingStyle.stroke,
|
||||||
|
QrCodeElement.finderPatternOuter,
|
||||||
|
position: position,
|
||||||
|
);
|
||||||
|
_paintCache.cache(
|
||||||
|
Paint()..style = PaintingStyle.stroke,
|
||||||
|
QrCodeElement.finderPatternInner,
|
||||||
|
position: position,
|
||||||
|
);
|
||||||
|
_paintCache.cache(
|
||||||
|
Paint()..style = PaintingStyle.fill,
|
||||||
|
QrCodeElement.finderPatternDot,
|
||||||
|
position: position,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
// if the widget has a zero size side then we cannot continue painting.
|
||||||
|
if (size.shortestSide == 0) {
|
||||||
|
debugPrint(
|
||||||
|
"[QR] WARN: width or height is zero. You should set a 'size' value "
|
||||||
|
'or nest this painter in a Widget that defines a non-zero size');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final paintMetrics = _PaintMetrics(
|
||||||
|
containerSize: size.shortestSide,
|
||||||
|
moduleCount: _qr!.moduleCount,
|
||||||
|
gapSize: gapless ? 0 : _gapSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
// draw the finder pattern elements
|
||||||
|
_drawFinderPatternItem(
|
||||||
|
FinderPatternPosition.topLeft,
|
||||||
|
canvas,
|
||||||
|
paintMetrics,
|
||||||
|
);
|
||||||
|
_drawFinderPatternItem(
|
||||||
|
FinderPatternPosition.bottomLeft,
|
||||||
|
canvas,
|
||||||
|
paintMetrics,
|
||||||
|
);
|
||||||
|
_drawFinderPatternItem(
|
||||||
|
FinderPatternPosition.topRight,
|
||||||
|
canvas,
|
||||||
|
paintMetrics,
|
||||||
|
);
|
||||||
|
|
||||||
|
// DEBUG: draw the inner content boundary
|
||||||
|
// final paint = Paint()..style = ui.PaintingStyle.stroke;
|
||||||
|
// paint.strokeWidth = 1;
|
||||||
|
// paint.color = const Color(0x55222222);
|
||||||
|
// canvas.drawRect(
|
||||||
|
// Rect.fromLTWH(paintMetrics.inset, paintMetrics.inset,
|
||||||
|
// paintMetrics.innerContentSize, paintMetrics.innerContentSize),
|
||||||
|
// paint);
|
||||||
|
|
||||||
|
Size? embeddedImageSize;
|
||||||
|
Offset? embeddedImagePosition;
|
||||||
|
Offset? safeAreaPosition;
|
||||||
|
Rect? safeAreaRect;
|
||||||
|
if (embeddedImage != null) {
|
||||||
|
final originalSize = Size(
|
||||||
|
embeddedImage!.width.toDouble(),
|
||||||
|
embeddedImage!.height.toDouble(),
|
||||||
|
);
|
||||||
|
final requestedSize = embeddedImageStyle.size;
|
||||||
|
embeddedImageSize = _scaledAspectSize(size, originalSize, requestedSize);
|
||||||
|
embeddedImagePosition = Offset(
|
||||||
|
(size.width - embeddedImageSize.width) / 2.0,
|
||||||
|
(size.height - embeddedImageSize.height) / 2.0,
|
||||||
|
);
|
||||||
|
if(embeddedImageStyle.safeArea) {
|
||||||
|
final safeAreaMultiplier = embeddedImageStyle.safeAreaMultiplier;
|
||||||
|
safeAreaPosition = Offset(
|
||||||
|
(size.width - embeddedImageSize.width * safeAreaMultiplier) / 2.0,
|
||||||
|
(size.height - embeddedImageSize.height * safeAreaMultiplier) / 2.0,
|
||||||
|
);
|
||||||
|
safeAreaRect = Rect.fromLTWH(
|
||||||
|
safeAreaPosition.dx,
|
||||||
|
safeAreaPosition.dy,
|
||||||
|
embeddedImageSize.width * safeAreaMultiplier,
|
||||||
|
embeddedImageSize.height * safeAreaMultiplier,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(embeddedImageStyle.embeddedImageShape != EmbeddedImageShape.none) {
|
||||||
|
final color = _priorityColor(embeddedImageStyle.shapeColor);
|
||||||
|
|
||||||
|
final squareRect = Rect.fromLTWH(
|
||||||
|
embeddedImagePosition.dx,
|
||||||
|
embeddedImagePosition.dy,
|
||||||
|
embeddedImageSize.width,
|
||||||
|
embeddedImageSize.height,
|
||||||
|
);
|
||||||
|
|
||||||
|
final paint = Paint()..color = color;
|
||||||
|
|
||||||
|
switch(embeddedImageStyle.embeddedImageShape) {
|
||||||
|
case EmbeddedImageShape.square:
|
||||||
|
if(embeddedImageStyle.borderRadius > 0) {
|
||||||
|
final roundedRect = RRect.fromRectAndRadius(
|
||||||
|
squareRect,
|
||||||
|
Radius.circular(embeddedImageStyle.borderRadius),
|
||||||
|
);
|
||||||
|
canvas.drawRRect(roundedRect, paint);
|
||||||
|
} else {
|
||||||
|
canvas.drawRect(squareRect, paint);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case EmbeddedImageShape.circle:
|
||||||
|
final roundedRect = RRect.fromRectAndRadius(squareRect,
|
||||||
|
Radius.circular(squareRect.width / 2));
|
||||||
|
canvas.drawRRect(roundedRect, paint);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final gap = !gapless ? _gapSize : 0;
|
||||||
|
// get the painters for the pixel information
|
||||||
|
final pixelPaint = _paintCache.firstPaint(QrCodeElement.codePixel);
|
||||||
|
pixelPaint!.color = _priorityColor(dataModuleStyle.color);
|
||||||
|
|
||||||
|
final emptyPixelPaint = _paintCache
|
||||||
|
.firstPaint(QrCodeElement.codePixelEmpty);
|
||||||
|
emptyPixelPaint!.color = _qrDefaultEmptyColor;
|
||||||
|
|
||||||
|
final borderRadius = Radius
|
||||||
|
.circular(dataModuleStyle.borderRadius);
|
||||||
|
final outsideBorderRadius = Radius
|
||||||
|
.circular(dataModuleStyle.outsideBorderRadius);
|
||||||
|
final isRoundedOutsideCorners = dataModuleStyle.roundedOutsideCorners;
|
||||||
|
|
||||||
|
for (var x = 0; x < _qr!.moduleCount; x++) {
|
||||||
|
for (var y = 0; y < _qr!.moduleCount; y++) {
|
||||||
|
// draw the finder patterns independently
|
||||||
|
if (_isFinderPatternPosition(x, y)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final isDark = _qrImage.isDark(y, x);
|
||||||
|
final paint = isDark ? pixelPaint : emptyPixelPaint;
|
||||||
|
if (!isDark && !isRoundedOutsideCorners) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// paint a pixel
|
||||||
|
final squareRect = _createDataModuleRect(paintMetrics, x, y, gap);
|
||||||
|
// check safeArea
|
||||||
|
if(embeddedImageStyle.safeArea
|
||||||
|
&& safeAreaRect?.overlaps(squareRect) == true) continue;
|
||||||
|
switch(dataModuleStyle.dataModuleShape) {
|
||||||
|
case QrDataModuleShape.square:
|
||||||
|
if(dataModuleStyle.borderRadius > 0) {
|
||||||
|
|
||||||
|
// If pixel isDark == true and outside safe area
|
||||||
|
// than can't be rounded
|
||||||
|
final isDarkLeft = _isDarkOnSide(x - 1, y,
|
||||||
|
safeAreaRect, paintMetrics, gap);
|
||||||
|
final isDarkTop = _isDarkOnSide(x, y - 1,
|
||||||
|
safeAreaRect, paintMetrics, gap);
|
||||||
|
final isDarkRight = _isDarkOnSide(x + 1, y,
|
||||||
|
safeAreaRect, paintMetrics, gap);
|
||||||
|
final isDarkBottom = _isDarkOnSide(x, y + 1,
|
||||||
|
safeAreaRect, paintMetrics, gap);
|
||||||
|
|
||||||
|
if(!isDark && isRoundedOutsideCorners) {
|
||||||
|
final isDarkTopLeft = _isDarkOnSide(x - 1, y - 1,
|
||||||
|
safeAreaRect, paintMetrics, gap);;
|
||||||
|
final isDarkTopRight = _isDarkOnSide(x + 1, y - 1,
|
||||||
|
safeAreaRect, paintMetrics, gap);;
|
||||||
|
final isDarkBottomLeft = _isDarkOnSide(x - 1, y + 1,
|
||||||
|
safeAreaRect, paintMetrics, gap);;
|
||||||
|
final isDarkBottomRight = _isDarkOnSide(x + 1, y + 1,
|
||||||
|
safeAreaRect, paintMetrics, gap);;
|
||||||
|
|
||||||
|
final roundedRect = RRect.fromRectAndCorners(
|
||||||
|
squareRect,
|
||||||
|
topLeft: isDarkTop && isDarkLeft && isDarkTopLeft
|
||||||
|
? outsideBorderRadius
|
||||||
|
: Radius.zero,
|
||||||
|
topRight: isDarkTop && isDarkRight && isDarkTopRight
|
||||||
|
? outsideBorderRadius
|
||||||
|
: Radius.zero,
|
||||||
|
bottomLeft: isDarkBottom && isDarkLeft && isDarkBottomLeft
|
||||||
|
? outsideBorderRadius
|
||||||
|
: Radius.zero,
|
||||||
|
bottomRight: isDarkBottom && isDarkRight && isDarkBottomRight
|
||||||
|
? outsideBorderRadius
|
||||||
|
: Radius.zero,
|
||||||
|
);
|
||||||
|
canvas.drawPath(
|
||||||
|
Path.combine(
|
||||||
|
PathOperation.difference,
|
||||||
|
Path()..addRect(squareRect),
|
||||||
|
Path()..addRRect(roundedRect)..close(),
|
||||||
|
),
|
||||||
|
pixelPaint,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final roundedRect = RRect.fromRectAndCorners(
|
||||||
|
squareRect,
|
||||||
|
topLeft: isDarkTop || isDarkLeft
|
||||||
|
? Radius.zero
|
||||||
|
: borderRadius,
|
||||||
|
topRight: isDarkTop || isDarkRight
|
||||||
|
? Radius.zero
|
||||||
|
: borderRadius,
|
||||||
|
bottomLeft: isDarkBottom || isDarkLeft
|
||||||
|
? Radius.zero
|
||||||
|
: borderRadius,
|
||||||
|
bottomRight: isDarkBottom || isDarkRight
|
||||||
|
? Radius.zero
|
||||||
|
: borderRadius,
|
||||||
|
);
|
||||||
|
canvas.drawRRect(roundedRect, paint);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
canvas.drawRect(squareRect, paint);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
final roundedRect = RRect.fromRectAndRadius(squareRect,
|
||||||
|
Radius.circular(squareRect.width / 2));
|
||||||
|
canvas.drawRRect(roundedRect, paint);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set gradient for all
|
||||||
|
if(gradient != null) {
|
||||||
|
final paintGradient = Paint();
|
||||||
|
paintGradient.shader = gradient!
|
||||||
|
.createShader(Rect.fromLTWH(0, 0, size.width, size.height));
|
||||||
|
paintGradient.blendMode = BlendMode.values[12];
|
||||||
|
canvas.drawRect(
|
||||||
|
Rect.fromLTWH(
|
||||||
|
paintMetrics.inset,
|
||||||
|
paintMetrics.inset,
|
||||||
|
paintMetrics.innerContentSize,
|
||||||
|
paintMetrics.innerContentSize,
|
||||||
|
),
|
||||||
|
paintGradient,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the image overlay.
|
||||||
|
if (embeddedImage != null) {
|
||||||
|
_drawImageOverlay(
|
||||||
|
canvas,
|
||||||
|
embeddedImagePosition!,
|
||||||
|
embeddedImageSize!,
|
||||||
|
embeddedImageStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isDarkOnSide(int x, int y, Rect? safeAreaRect,
|
||||||
|
_PaintMetrics paintMetrics, num gap,) {
|
||||||
|
final maxIndexPixel = _qrImage.moduleCount - 1;
|
||||||
|
|
||||||
|
final xIsContains = x >= 0 && x <= maxIndexPixel;
|
||||||
|
final yIsContains = y >= 0 && y <= maxIndexPixel;
|
||||||
|
|
||||||
|
return xIsContains && yIsContains
|
||||||
|
? _qrImage.isDark(y, x)
|
||||||
|
&& !(safeAreaRect?.overlaps(
|
||||||
|
_createDataModuleRect(paintMetrics, x, y, gap))
|
||||||
|
?? false)
|
||||||
|
: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect _createDataModuleRect(_PaintMetrics paintMetrics, int x, int y, num gap) {
|
||||||
|
final left = paintMetrics.inset + (x * (paintMetrics.pixelSize + gap));
|
||||||
|
final top = paintMetrics.inset + (y * (paintMetrics.pixelSize + gap));
|
||||||
|
var pixelHTweak = 0.0;
|
||||||
|
var pixelVTweak = 0.0;
|
||||||
|
if (gapless && _hasAdjacentHorizontalPixel(x, y, _qr!.moduleCount)) {
|
||||||
|
pixelHTweak = 0.5;
|
||||||
|
}
|
||||||
|
if (gapless && _hasAdjacentVerticalPixel(x, y, _qr!.moduleCount)) {
|
||||||
|
pixelVTweak = 0.5;
|
||||||
|
}
|
||||||
|
return Rect.fromLTWH(
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
paintMetrics.pixelSize + pixelHTweak,
|
||||||
|
paintMetrics.pixelSize + pixelVTweak,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasAdjacentVerticalPixel(int x, int y, int moduleCount) {
|
||||||
|
if (y + 1 >= moduleCount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return _qrImage.isDark(y + 1, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasAdjacentHorizontalPixel(int x, int y, int moduleCount) {
|
||||||
|
if (x + 1 >= moduleCount) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return _qrImage.isDark(y, x + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isFinderPatternPosition(int x, int y) {
|
||||||
|
final isTopLeft = y < _finderPatternLimit && x < _finderPatternLimit;
|
||||||
|
final isBottomLeft = y < _finderPatternLimit &&
|
||||||
|
(x >= _qr!.moduleCount - _finderPatternLimit);
|
||||||
|
final isTopRight = y >= _qr!.moduleCount - _finderPatternLimit &&
|
||||||
|
(x < _finderPatternLimit);
|
||||||
|
return isTopLeft || isBottomLeft || isTopRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawFinderPatternItem(
|
||||||
|
FinderPatternPosition position,
|
||||||
|
Canvas canvas,
|
||||||
|
_PaintMetrics metrics,
|
||||||
|
) {
|
||||||
|
final totalGap = (_finderPatternLimit - 1) * metrics.gapSize;
|
||||||
|
final radius =
|
||||||
|
((_finderPatternLimit * metrics.pixelSize) + totalGap) -
|
||||||
|
metrics.pixelSize;
|
||||||
|
final strokeAdjust = metrics.pixelSize / 2.0;
|
||||||
|
final edgePos =
|
||||||
|
(metrics.inset + metrics.innerContentSize) - (radius + strokeAdjust);
|
||||||
|
|
||||||
|
Offset offset;
|
||||||
|
if (position == FinderPatternPosition.topLeft) {
|
||||||
|
offset =
|
||||||
|
Offset(metrics.inset + strokeAdjust, metrics.inset + strokeAdjust);
|
||||||
|
} else if (position == FinderPatternPosition.bottomLeft) {
|
||||||
|
offset = Offset(metrics.inset + strokeAdjust, edgePos);
|
||||||
|
} else {
|
||||||
|
offset = Offset(edgePos, metrics.inset + strokeAdjust);
|
||||||
|
}
|
||||||
|
|
||||||
|
// configure the paints
|
||||||
|
final outerPaint = _paintCache.firstPaint(
|
||||||
|
QrCodeElement.finderPatternOuter,
|
||||||
|
position: position,
|
||||||
|
)!;
|
||||||
|
final color = _priorityColor(eyeStyle.color);
|
||||||
|
outerPaint.strokeWidth = metrics.pixelSize;
|
||||||
|
outerPaint.color = color;
|
||||||
|
|
||||||
|
final innerPaint = _paintCache
|
||||||
|
.firstPaint(QrCodeElement.finderPatternInner, position: position)!;
|
||||||
|
innerPaint.strokeWidth = metrics.pixelSize;
|
||||||
|
innerPaint.color = emptyColor;
|
||||||
|
|
||||||
|
final dotPaint = _paintCache.firstPaint(
|
||||||
|
QrCodeElement.finderPatternDot,
|
||||||
|
position: position,
|
||||||
|
);
|
||||||
|
dotPaint!.color = color;
|
||||||
|
|
||||||
|
final outerRect =
|
||||||
|
Rect.fromLTWH(offset.dx, offset.dy, radius, radius);
|
||||||
|
|
||||||
|
final innerRadius = radius - (2 * metrics.pixelSize);
|
||||||
|
final innerRect = Rect.fromLTWH(
|
||||||
|
offset.dx + metrics.pixelSize,
|
||||||
|
offset.dy + metrics.pixelSize,
|
||||||
|
innerRadius,
|
||||||
|
innerRadius,
|
||||||
|
);
|
||||||
|
|
||||||
|
final gap = metrics.pixelSize * 2;
|
||||||
|
final dotSize = radius - gap - (2 * strokeAdjust);
|
||||||
|
final dotRect = Rect.fromLTWH(
|
||||||
|
offset.dx + metrics.pixelSize + strokeAdjust,
|
||||||
|
offset.dy + metrics.pixelSize + strokeAdjust,
|
||||||
|
dotSize,
|
||||||
|
dotSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
switch(eyeStyle.eyeShape) {
|
||||||
|
case QrEyeShape.square:
|
||||||
|
if(eyeStyle.borderRadius > 0) {
|
||||||
|
final roundedOuterStrokeRect = RRect.fromRectAndRadius(
|
||||||
|
outerRect, Radius.circular(eyeStyle.borderRadius));
|
||||||
|
canvas.drawRRect(roundedOuterStrokeRect, outerPaint);
|
||||||
|
canvas.drawRect(innerRect, innerPaint);
|
||||||
|
final roundedDotStrokeRect = RRect.fromRectAndRadius(
|
||||||
|
dotRect, Radius.circular(eyeStyle.borderRadius / 2));
|
||||||
|
canvas.drawRRect(roundedDotStrokeRect, dotPaint);
|
||||||
|
} else {
|
||||||
|
canvas.drawRect(outerRect, outerPaint);
|
||||||
|
canvas.drawRect(innerRect, innerPaint);
|
||||||
|
canvas.drawRect(dotRect, dotPaint);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
final roundedOuterStrokeRect =
|
||||||
|
RRect.fromRectAndRadius(outerRect, Radius.circular(radius));
|
||||||
|
canvas.drawRRect(roundedOuterStrokeRect, outerPaint);
|
||||||
|
|
||||||
|
final roundedInnerStrokeRect =
|
||||||
|
RRect.fromRectAndRadius(outerRect, Radius.circular(innerRadius));
|
||||||
|
canvas.drawRRect(roundedInnerStrokeRect, innerPaint);
|
||||||
|
|
||||||
|
final roundedDotStrokeRect =
|
||||||
|
RRect.fromRectAndRadius(dotRect, Radius.circular(dotSize));
|
||||||
|
canvas.drawRRect(roundedDotStrokeRect, dotPaint);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasOneNonZeroSide(Size size) => size.longestSide > 0;
|
||||||
|
|
||||||
|
Size _scaledAspectSize(
|
||||||
|
Size widgetSize,
|
||||||
|
Size originalSize,
|
||||||
|
Size? requestedSize,
|
||||||
|
) {
|
||||||
|
if (requestedSize != null && !requestedSize.isEmpty) {
|
||||||
|
return requestedSize;
|
||||||
|
} else if (requestedSize != null && _hasOneNonZeroSide(requestedSize)) {
|
||||||
|
final maxSide = requestedSize.longestSide;
|
||||||
|
final ratio = maxSide / originalSize.longestSide;
|
||||||
|
return Size(ratio * originalSize.width, ratio * originalSize.height);
|
||||||
|
} else {
|
||||||
|
final maxSide = 0.25 * widgetSize.shortestSide;
|
||||||
|
final ratio = maxSide / originalSize.longestSide;
|
||||||
|
return Size(ratio * originalSize.width, ratio * originalSize.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _drawImageOverlay(
|
||||||
|
Canvas canvas,
|
||||||
|
Offset position,
|
||||||
|
Size size,
|
||||||
|
QrEmbeddedImageStyle? style,
|
||||||
|
) {
|
||||||
|
final paint = Paint()
|
||||||
|
..isAntiAlias = true
|
||||||
|
..filterQuality = FilterQuality.high;
|
||||||
|
if (style != null) {
|
||||||
|
if (style.color != null) {
|
||||||
|
paint.colorFilter = ColorFilter.mode(style.color!, BlendMode.srcATop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final srcSize = Size(
|
||||||
|
embeddedImage!.width.toDouble(),
|
||||||
|
embeddedImage!.height.toDouble(),
|
||||||
|
);
|
||||||
|
final src = Alignment.center.inscribe(srcSize, Offset.zero & srcSize);
|
||||||
|
final dst = Alignment.center.inscribe(size, position & size);
|
||||||
|
canvas.drawImageRect(embeddedImage!, src, dst, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// if [gradient] != null, then only black [_qrDefaultColor],
|
||||||
|
/// needed for gradient
|
||||||
|
/// else [color] or [QrPainter.color]
|
||||||
|
Color _priorityColor(Color? color) =>
|
||||||
|
gradient != null ? _qrDefaultColor : color ?? this.color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(CustomPainter oldPainter) {
|
||||||
|
if (oldPainter is QrPainter) {
|
||||||
|
return errorCorrectionLevel != oldPainter.errorCorrectionLevel ||
|
||||||
|
_calcVersion != oldPainter._calcVersion ||
|
||||||
|
_qr != oldPainter._qr ||
|
||||||
|
gapless != oldPainter.gapless ||
|
||||||
|
embeddedImage != oldPainter.embeddedImage ||
|
||||||
|
embeddedImageStyle != oldPainter.embeddedImageStyle ||
|
||||||
|
eyeStyle != oldPainter.eyeStyle ||
|
||||||
|
dataModuleStyle != oldPainter.dataModuleStyle;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a [ui.Picture] object containing the QR code data.
|
||||||
|
ui.Picture toPicture(double size) {
|
||||||
|
final recorder = ui.PictureRecorder();
|
||||||
|
final canvas = Canvas(recorder);
|
||||||
|
paint(canvas, Size(size, size));
|
||||||
|
return recorder.endRecording();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the raw QR code [ui.Image] object.
|
||||||
|
Future<ui.Image> toImage(double size) {
|
||||||
|
return toPicture(size).toImage(size.toInt(), size.toInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the raw QR code image byte data.
|
||||||
|
Future<ByteData?> toImageData(
|
||||||
|
double size, {
|
||||||
|
ui.ImageByteFormat format = ui.ImageByteFormat.png,
|
||||||
|
}) async {
|
||||||
|
final image = await toImage(size);
|
||||||
|
return image.toByteData(format: format);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PaintMetrics {
|
||||||
|
_PaintMetrics({
|
||||||
|
required this.containerSize,
|
||||||
|
required this.gapSize,
|
||||||
|
required this.moduleCount,
|
||||||
|
}) {
|
||||||
|
_calculateMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
final int moduleCount;
|
||||||
|
final double containerSize;
|
||||||
|
final double gapSize;
|
||||||
|
|
||||||
|
late final double _pixelSize;
|
||||||
|
double get pixelSize => _pixelSize;
|
||||||
|
|
||||||
|
late final double _innerContentSize;
|
||||||
|
double get innerContentSize => _innerContentSize;
|
||||||
|
|
||||||
|
late final double _inset;
|
||||||
|
double get inset => _inset;
|
||||||
|
|
||||||
|
void _calculateMetrics() {
|
||||||
|
final gapTotal = (moduleCount - 1) * gapSize;
|
||||||
|
final pixelSize = (containerSize - gapTotal) / moduleCount;
|
||||||
|
_pixelSize = (pixelSize * 2).roundToDouble() / 2;
|
||||||
|
_innerContentSize = (_pixelSize * moduleCount) + gapTotal;
|
||||||
|
_inset = (containerSize - _innerContentSize) / 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
qr_flutter/lib/src/qr_versions.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* QR.Flutter
|
||||||
|
* Copyright (c) 2019 the QR.Flutter authors.
|
||||||
|
* See LICENSE for distribution and usage details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// This class only contains special version codes. QR codes support version
|
||||||
|
/// numbers from 1-40 and you should just use the numeric version directly.
|
||||||
|
class QrVersions {
|
||||||
|
/// Automatically determine the QR code version based on input and an
|
||||||
|
/// error correction level.
|
||||||
|
static const int auto = -1;
|
||||||
|
|
||||||
|
/// The minimum supported version code.
|
||||||
|
static const int min = 1;
|
||||||
|
|
||||||
|
/// The maximum supported version code.
|
||||||
|
static const int max = 40;
|
||||||
|
|
||||||
|
/// Checks to see if the supplied version is a valid QR code version
|
||||||
|
static bool isSupportedVersion(int version) =>
|
||||||
|
version == auto || (version >= min && version <= max);
|
||||||
|
}
|
||||||
208
qr_flutter/lib/src/types.dart
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
/*
|
||||||
|
* QR.Flutter
|
||||||
|
* Copyright (c) 2019 the QR.Flutter authors.
|
||||||
|
* See LICENSE for distribution and usage details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
|
||||||
|
/// Represents a specific element / part of a QR code. This is used to isolate
|
||||||
|
/// the different parts so that we can style and modify specific parts
|
||||||
|
/// independently.
|
||||||
|
enum QrCodeElement {
|
||||||
|
/// The 'stroke' / outer square of the QR code finder pattern element.
|
||||||
|
finderPatternOuter,
|
||||||
|
|
||||||
|
/// The inner/in-between square of the QR code finder pattern element.
|
||||||
|
finderPatternInner,
|
||||||
|
|
||||||
|
/// The "dot" square of the QR code finder pattern element.
|
||||||
|
finderPatternDot,
|
||||||
|
|
||||||
|
/// The individual pixels of the QR code
|
||||||
|
codePixel,
|
||||||
|
|
||||||
|
/// The "empty" pixels of the QR code
|
||||||
|
codePixelEmpty,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enumeration representing the three finder pattern (square 'eye') locations.
|
||||||
|
enum FinderPatternPosition {
|
||||||
|
/// The top left position.
|
||||||
|
topLeft,
|
||||||
|
|
||||||
|
/// The top right position.
|
||||||
|
topRight,
|
||||||
|
|
||||||
|
/// The bottom left position.
|
||||||
|
bottomLeft,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enumeration representing the finder pattern eye's shape.
|
||||||
|
enum QrEyeShape {
|
||||||
|
/// Use square eye frame.
|
||||||
|
square,
|
||||||
|
|
||||||
|
/// Use circular eye frame.
|
||||||
|
circle,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enumeration representing the shape of Data modules inside QR.
|
||||||
|
enum QrDataModuleShape {
|
||||||
|
/// Use square dots.
|
||||||
|
square,
|
||||||
|
|
||||||
|
/// Use circular dots.
|
||||||
|
circle,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enumeration representing the shape behind embedded picture
|
||||||
|
enum EmbeddedImageShape {
|
||||||
|
/// Disable
|
||||||
|
none,
|
||||||
|
|
||||||
|
/// Use square.
|
||||||
|
square,
|
||||||
|
|
||||||
|
/// Use circular.
|
||||||
|
circle,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Styling options for finder pattern eye.
|
||||||
|
@immutable
|
||||||
|
class QrEyeStyle {
|
||||||
|
/// Create a new set of styling options for QR Eye.
|
||||||
|
const QrEyeStyle({
|
||||||
|
this.eyeShape = QrEyeShape.square,
|
||||||
|
this.color,
|
||||||
|
this.borderRadius = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Eye shape.
|
||||||
|
final QrEyeShape eyeShape;
|
||||||
|
|
||||||
|
/// Color to tint the eye.
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
/// Border radius
|
||||||
|
final double borderRadius;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => eyeShape.hashCode ^ color.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other is QrEyeStyle) {
|
||||||
|
return eyeShape == other.eyeShape && color == other.color;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Styling options for data module.
|
||||||
|
@immutable
|
||||||
|
class QrDataModuleStyle {
|
||||||
|
/// Create a new set of styling options for data modules.
|
||||||
|
const QrDataModuleStyle({
|
||||||
|
this.dataModuleShape = QrDataModuleShape.square,
|
||||||
|
this.color,
|
||||||
|
this.borderRadius = 0,
|
||||||
|
this.roundedOutsideCorners = false,
|
||||||
|
double? outsideBorderRadius,
|
||||||
|
}) : _outsideBorderRadius = outsideBorderRadius;
|
||||||
|
|
||||||
|
/// Data module shape.
|
||||||
|
final QrDataModuleShape dataModuleShape;
|
||||||
|
|
||||||
|
/// Color to tint the data modules.
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
/// Border radius
|
||||||
|
final double borderRadius;
|
||||||
|
|
||||||
|
/// Only [QrDataModuleShape.square]
|
||||||
|
/// true for rounded outside corners
|
||||||
|
final bool roundedOutsideCorners;
|
||||||
|
|
||||||
|
/// Only [QrDataModuleShape.square]
|
||||||
|
/// Border radius for outside corners
|
||||||
|
final double? _outsideBorderRadius;
|
||||||
|
|
||||||
|
/// if [roundedOutsideCorners] == true, then by default use [borderRadius]
|
||||||
|
/// [_outsideBorderRadius] <= [borderRadius]
|
||||||
|
/// Get border radius for outside corners
|
||||||
|
double get outsideBorderRadius {
|
||||||
|
if(roundedOutsideCorners) {
|
||||||
|
return _outsideBorderRadius != null
|
||||||
|
&& _outsideBorderRadius! < borderRadius
|
||||||
|
? _outsideBorderRadius! : borderRadius;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => dataModuleShape.hashCode ^ color.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other is QrDataModuleStyle) {
|
||||||
|
return dataModuleShape == other.dataModuleShape && color == other.color;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Styling options for any embedded image overlay
|
||||||
|
@immutable
|
||||||
|
class QrEmbeddedImageStyle {
|
||||||
|
/// Create a new set of styling options.
|
||||||
|
const QrEmbeddedImageStyle({
|
||||||
|
this.size,
|
||||||
|
this.color,
|
||||||
|
this.safeArea = false,
|
||||||
|
this.safeAreaMultiplier = 1,
|
||||||
|
this.embeddedImageShape = EmbeddedImageShape.none,
|
||||||
|
this.shapeColor,
|
||||||
|
this.borderRadius = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The size of the image. If one dimension is zero then the other dimension
|
||||||
|
/// will be used to scale the zero dimension based on the original image
|
||||||
|
/// size.
|
||||||
|
final Size? size;
|
||||||
|
|
||||||
|
/// Color to tint the image.
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
/// Hide data modules behind embedded image.
|
||||||
|
/// Data modules are not displayed inside area
|
||||||
|
final bool safeArea;
|
||||||
|
|
||||||
|
/// Safe area size multiplier.
|
||||||
|
final double safeAreaMultiplier;
|
||||||
|
|
||||||
|
/// Shape background embedded image
|
||||||
|
final EmbeddedImageShape embeddedImageShape;
|
||||||
|
|
||||||
|
/// Border radius shape
|
||||||
|
final double borderRadius;
|
||||||
|
|
||||||
|
/// Color background
|
||||||
|
final Color? shapeColor;
|
||||||
|
|
||||||
|
/// Check to see if the style object has a non-null, non-zero size.
|
||||||
|
bool get hasDefinedSize => size != null && size!.longestSide > 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => size.hashCode ^ color.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (other is QrEmbeddedImageStyle) {
|
||||||
|
return size == other.size && color == other.color;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
qr_flutter/lib/src/validator.dart
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* QR.Flutter
|
||||||
|
* Copyright (c) 2019 the QR.Flutter authors.
|
||||||
|
* See LICENSE for distribution and usage details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:qr/qr.dart';
|
||||||
|
|
||||||
|
import 'qr_versions.dart';
|
||||||
|
|
||||||
|
/// A utility class for validating and pre-rendering QR code data.
|
||||||
|
class QrValidator {
|
||||||
|
/// Attempt to parse / generate the QR code data and check for any errors. The
|
||||||
|
/// resulting [QrValidationResult] object will hold the status of the QR code
|
||||||
|
/// as well as the generated QR code data.
|
||||||
|
static QrValidationResult validate({
|
||||||
|
required String data,
|
||||||
|
int version = QrVersions.auto,
|
||||||
|
int errorCorrectionLevel = QrErrorCorrectLevel.L,
|
||||||
|
}) {
|
||||||
|
late final QrCode qrCode;
|
||||||
|
try {
|
||||||
|
if (version != QrVersions.auto) {
|
||||||
|
qrCode = QrCode(version, errorCorrectionLevel);
|
||||||
|
qrCode.addData(data);
|
||||||
|
} else {
|
||||||
|
qrCode = QrCode.fromData(
|
||||||
|
data: data,
|
||||||
|
errorCorrectLevel: errorCorrectionLevel,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return QrValidationResult(
|
||||||
|
status: QrValidationStatus.valid,
|
||||||
|
qrCode: qrCode,
|
||||||
|
);
|
||||||
|
} on InputTooLongException catch (title) {
|
||||||
|
return QrValidationResult(
|
||||||
|
status: QrValidationStatus.contentTooLong,
|
||||||
|
error: title,
|
||||||
|
);
|
||||||
|
} on Exception catch (ex) {
|
||||||
|
return QrValidationResult(status: QrValidationStatus.error, error: ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Captures the status or a QR code validation operations, as well as the
|
||||||
|
/// rendered and validated data / object so that it can be used in any
|
||||||
|
/// secondary operations (to avoid re-rendering). It also keeps any exception
|
||||||
|
/// that was thrown.
|
||||||
|
class QrValidationResult {
|
||||||
|
/// Create a new validation result instance.
|
||||||
|
QrValidationResult({required this.status, this.qrCode, this.error});
|
||||||
|
|
||||||
|
/// The status of the validation operation.
|
||||||
|
QrValidationStatus status;
|
||||||
|
|
||||||
|
/// The rendered QR code data / object.
|
||||||
|
QrCode? qrCode;
|
||||||
|
|
||||||
|
/// The exception that was thrown in the event of a non-valid result (if any).
|
||||||
|
Exception? error;
|
||||||
|
|
||||||
|
/// The validation result returned a status of valid;
|
||||||
|
bool get isValid => status == QrValidationStatus.valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The status of the QR code data you requested to be validated.
|
||||||
|
enum QrValidationStatus {
|
||||||
|
/// The QR code data is valid for the provided parameters.
|
||||||
|
valid,
|
||||||
|
|
||||||
|
/// The QR code data is too long for the provided version + error check
|
||||||
|
/// configuration or too long to be contained in a QR code.
|
||||||
|
contentTooLong,
|
||||||
|
|
||||||
|
/// An unknown / unexpected error occurred when we tried to validate the QR
|
||||||
|
/// code data.
|
||||||
|
error,
|
||||||
|
}
|
||||||
197
qr_flutter/pubspec.lock
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.13.0"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.0"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.3"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker
|
||||||
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.2"
|
||||||
|
leak_tracker_flutter_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_flutter_testing
|
||||||
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.10"
|
||||||
|
leak_tracker_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_testing
|
||||||
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.17"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.11.1"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.17.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
qr:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: qr
|
||||||
|
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.1"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.7"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "15.0.2"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.8.0-0 <4.0.0"
|
||||||
|
flutter: ">=3.18.0-18.0.pre.54"
|
||||||
22
qr_flutter/pubspec.yaml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
name: qr_flutter
|
||||||
|
description: >
|
||||||
|
QR.Flutter is a Flutter library for simple and fast QR code rendering via a
|
||||||
|
Widget or custom painter.
|
||||||
|
version: 4.1.0
|
||||||
|
homepage: https://github.com/theyakka/qr.flutter
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=2.19.6 <4.0.0'
|
||||||
|
flutter: ">=3.7.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
qr: ^3.0.1
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: false
|
||||||
BIN
qr_flutter/test/.golden/qr_image_data_module_styled_golden.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 40 KiB |
BIN
qr_flutter/test/.golden/qr_image_eye_styled_golden.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
qr_flutter/test/.golden/qr_image_foreground_colored_golden.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
qr_flutter/test/.golden/qr_image_golden.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
qr_flutter/test/.golden/qr_image_logo_golden.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
qr_flutter/test/.golden/qr_painter_golden.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
qr_flutter/test/.images/logo_yakka.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
14
qr_flutter/test/all_test.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
/*
|
||||||
|
* QR.Flutter
|
||||||
|
* Copyright (c) 2019 the QR.Flutter authors.
|
||||||
|
* See LICENSE for distribution and usage details.
|
||||||
|
*/
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'image_test.dart' as image;
|
||||||
|
import 'painter_test.dart' as painter;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('image:', image.main);
|
||||||
|
group('painter:', painter.main);
|
||||||
|
}
|
||||||
218
qr_flutter/test/image_test.dart
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
/*
|
||||||
|
* QR.Flutter
|
||||||
|
* Copyright (c) 2019 the QR.Flutter authors.
|
||||||
|
* See LICENSE for distribution and usage details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('QrImageView generates correct image', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final qrImage = MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: RepaintBoundary(
|
||||||
|
child: QrImageView(
|
||||||
|
data: 'This is a test image',
|
||||||
|
version: QrVersions.auto,
|
||||||
|
gapless: true,
|
||||||
|
errorCorrectionLevel: QrErrorCorrectLevel.L,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(qrImage);
|
||||||
|
await expectLater(
|
||||||
|
find.byType(QrImageView),
|
||||||
|
matchesGoldenFile('./.golden/qr_image_golden.png'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'QrImageView generates correct image with eye style',
|
||||||
|
(tester) async {
|
||||||
|
final qrImage = MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: RepaintBoundary(
|
||||||
|
child: QrImageView(
|
||||||
|
data: 'This is a test image',
|
||||||
|
version: QrVersions.auto,
|
||||||
|
gapless: true,
|
||||||
|
errorCorrectionLevel: QrErrorCorrectLevel.L,
|
||||||
|
eyeStyle: const QrEyeStyle(
|
||||||
|
eyeShape: QrEyeShape.circle,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(qrImage);
|
||||||
|
await expectLater(
|
||||||
|
find.byType(QrImageView),
|
||||||
|
matchesGoldenFile('./.golden/qr_image_eye_styled_golden.png'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'QrImageView generates correct image with data module style',
|
||||||
|
(tester) async {
|
||||||
|
final qrImage = MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: RepaintBoundary(
|
||||||
|
child: QrImageView(
|
||||||
|
data: 'This is a test image',
|
||||||
|
version: QrVersions.auto,
|
||||||
|
gapless: true,
|
||||||
|
errorCorrectionLevel: QrErrorCorrectLevel.L,
|
||||||
|
dataModuleStyle: const QrDataModuleStyle(
|
||||||
|
dataModuleShape: QrDataModuleShape.circle,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(qrImage);
|
||||||
|
await expectLater(
|
||||||
|
find.byType(QrImageView),
|
||||||
|
matchesGoldenFile('./.golden/qr_image_data_module_styled_golden.png'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'QrImageView generates correct image with eye and data module sytle',
|
||||||
|
(tester) async {
|
||||||
|
final qrImage = MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: RepaintBoundary(
|
||||||
|
child: QrImageView(
|
||||||
|
data: 'This is a test image',
|
||||||
|
version: QrVersions.auto,
|
||||||
|
gapless: true,
|
||||||
|
errorCorrectionLevel: QrErrorCorrectLevel.L,
|
||||||
|
eyeStyle: const QrEyeStyle(
|
||||||
|
eyeShape: QrEyeShape.circle,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
dataModuleStyle: const QrDataModuleStyle(
|
||||||
|
dataModuleShape: QrDataModuleShape.circle,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(qrImage);
|
||||||
|
await expectLater(
|
||||||
|
find.byType(QrImageView),
|
||||||
|
matchesGoldenFile(
|
||||||
|
'./.golden/qr_image_eye_data_module_styled_golden.png',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'QrImageView does not apply eye and data module color when foreground '
|
||||||
|
'color is also specified',
|
||||||
|
(tester) async {
|
||||||
|
final qrImage = MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: RepaintBoundary(
|
||||||
|
child: QrImageView(
|
||||||
|
data: 'This is a test image',
|
||||||
|
version: QrVersions.auto,
|
||||||
|
gapless: true,
|
||||||
|
// ignore: deprecated_member_use_from_same_package
|
||||||
|
foregroundColor: Colors.red,
|
||||||
|
errorCorrectionLevel: QrErrorCorrectLevel.L,
|
||||||
|
eyeStyle: const QrEyeStyle(
|
||||||
|
eyeShape: QrEyeShape.circle,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
dataModuleStyle: const QrDataModuleStyle(
|
||||||
|
dataModuleShape: QrDataModuleShape.circle,
|
||||||
|
color: Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(qrImage);
|
||||||
|
await expectLater(
|
||||||
|
find.byType(QrImageView),
|
||||||
|
matchesGoldenFile('./.golden/qr_image_foreground_colored_golden.png'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'QrImageView generates correct image with logo',
|
||||||
|
(tester) async {
|
||||||
|
await pumpWidgetWithImages(
|
||||||
|
tester,
|
||||||
|
MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: RepaintBoundary(
|
||||||
|
child: QrImageView(
|
||||||
|
data: 'This is a a qr code with a logo',
|
||||||
|
version: QrVersions.auto,
|
||||||
|
gapless: true,
|
||||||
|
errorCorrectionLevel: QrErrorCorrectLevel.L,
|
||||||
|
embeddedImage: FileImage(File('test/.images/logo_yakka.png')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
<String>['test/.images/logo_yakka.png'],
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
find.byType(QrImageView),
|
||||||
|
matchesGoldenFile('./.golden/qr_image_logo_golden.png'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pre-cache images to make sure they show up in golden tests.
|
||||||
|
///
|
||||||
|
/// See https://github.com/flutter/flutter/issues/36552 for more info.
|
||||||
|
Future<void> pumpWidgetWithImages(
|
||||||
|
WidgetTester tester,
|
||||||
|
Widget widget,
|
||||||
|
List<String> assetNames,
|
||||||
|
) async {
|
||||||
|
Future<void>? precacheFuture;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
Builder(
|
||||||
|
builder: (buildContext) {
|
||||||
|
precacheFuture = tester.runAsync(() async {
|
||||||
|
await Future.wait(<Future<void>>[
|
||||||
|
for (final String assetName in assetNames)
|
||||||
|
precacheImage(FileImage(File(assetName)), buildContext),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
return widget;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await precacheFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTestableWidget(Widget widget) {
|
||||||
|
return MediaQuery(
|
||||||
|
data: const MediaQueryData(),
|
||||||
|
child: MaterialApp(home: widget),
|
||||||
|
);
|
||||||
|
}
|
||||||
41
qr_flutter/test/painter_test.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* QR.Flutter
|
||||||
|
* Copyright (c) 2019 the QR.Flutter authors.
|
||||||
|
* See LICENSE for distribution and usage details.
|
||||||
|
*/
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('QrPainter generates correct image', (tester) async {
|
||||||
|
final painter = QrPainter(
|
||||||
|
data: 'The painter is this thing',
|
||||||
|
version: QrVersions.auto,
|
||||||
|
gapless: true,
|
||||||
|
errorCorrectionLevel: QrErrorCorrectLevel.L,
|
||||||
|
);
|
||||||
|
ByteData? imageData;
|
||||||
|
await tester.runAsync(() async {
|
||||||
|
imageData = await painter.toImageData(600.0);
|
||||||
|
});
|
||||||
|
final imageBytes = imageData!.buffer.asUint8List();
|
||||||
|
final Widget widget = Center(
|
||||||
|
child: RepaintBoundary(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 600,
|
||||||
|
height: 600,
|
||||||
|
child: Image.memory(imageBytes),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpWidget(widget);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await expectLater(
|
||||||
|
find.byType(RepaintBoundary),
|
||||||
|
matchesGoldenFile('./.golden/qr_painter_golden.png'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||