add new dependency
|
|
@ -11,4 +11,5 @@ optional: 71c638891ce4f2aff35c7387727989f31f9d877d
|
|||
photo_view: a13ca2fc387a3fb1276126959e092c44d0029987
|
||||
pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd
|
||||
qr: ff808bb3f354e6a7029ec953cbe0144a42021db6
|
||||
qr_flutter: d5e7206396105d643113618290bbcc755d05f492
|
||||
x25519: ecb1d357714537bba6e276ef45f093846d4beaee
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
qr_flutter:
|
||||
git: https://github.com/theyakka/qr.flutter.git
|
||||
dependencies:
|
||||
qr:
|
||||
git: https://github.com/kevmoo/qr.dart.git
|
||||
|
||||
|
|
|
|||
|
|
@ -25,5 +25,7 @@ dependency_overrides:
|
|||
path: ./dependencies/pointycastle
|
||||
qr:
|
||||
path: ./dependencies/qr
|
||||
qr_flutter:
|
||||
path: ./dependencies/qr_flutter
|
||||
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'),
|
||||
);
|
||||
});
|
||||
}
|
||||