diff --git a/config.lock.yaml b/config.lock.yaml index 5dd4b2d..9274ef1 100644 --- a/config.lock.yaml +++ b/config.lock.yaml @@ -11,4 +11,5 @@ optional: 71c638891ce4f2aff35c7387727989f31f9d877d photo_view: a13ca2fc387a3fb1276126959e092c44d0029987 pointycastle: bbd8569f68a7fccbdf0b92d0b44a9219c126c8dd qr: ff808bb3f354e6a7029ec953cbe0144a42021db6 +qr_flutter: d5e7206396105d643113618290bbcc755d05f492 x25519: ecb1d357714537bba6e276ef45f093846d4beaee diff --git a/config.yaml b/config.yaml index 787f666..6b2df32 100644 --- a/config.yaml +++ b/config.yaml @@ -1,5 +1,8 @@ -qr: - git: https://github.com/kevmoo/qr.dart.git +qr_flutter: + git: https://github.com/theyakka/qr.flutter.git + dependencies: + qr: + git: https://github.com/kevmoo/qr.dart.git mutex: git: https://github.com/hoylen/dart-mutex.git diff --git a/pubspec.yaml b/pubspec.yaml index 450a64f..144765b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -25,5 +25,7 @@ dependency_overrides: path: ./dependencies/pointycastle qr: path: ./dependencies/qr + qr_flutter: + path: ./dependencies/qr_flutter x25519: path: ./dependencies/x25519 diff --git a/qr_flutter/LICENSE b/qr_flutter/LICENSE new file mode 100644 index 0000000..172ec64 --- /dev/null +++ b/qr_flutter/LICENSE @@ -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. diff --git a/qr_flutter/lib/qr_flutter.dart b/qr_flutter/lib/qr_flutter.dart new file mode 100644 index 0000000..6be9df6 --- /dev/null +++ b/qr_flutter/lib/qr_flutter.dart @@ -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'; diff --git a/qr_flutter/lib/src/errors.dart b/qr_flutter/lib/src/errors.dart new file mode 100644 index 0000000..239b9ea --- /dev/null +++ b/qr_flutter/lib/src/errors.dart @@ -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'; +} diff --git a/qr_flutter/lib/src/paint_cache.dart b/qr_flutter/lib/src/paint_cache.dart new file mode 100644 index 0000000..ead3df3 --- /dev/null +++ b/qr_flutter/lib/src/paint_cache.dart @@ -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 _pixelPaints = []; + final Map _keyedPaints = {}; + + 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 paints( + QrCodeElement element, { + FinderPatternPosition? position, + }) { + return element == QrCodeElement.codePixel + ? _pixelPaints + : [_keyedPaints[_cacheKey(element, position: position)]]; + } +} diff --git a/qr_flutter/lib/src/qr_image_view.dart b/qr_flutter/lib/src/qr_image_view.dart new file mode 100644 index 0000000..357d6ca --- /dev/null +++ b/qr_flutter/lib/src/qr_image_view.dart @@ -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 createState() => _QrImageViewState(); +} + +class _QrImageViewState extends State { + /// 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( + 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 _loadQrImage( + BuildContext buildContext, + QrEmbeddedImageStyle? style, + ) { + if (style != null) {} + + final mq = MediaQuery.of(buildContext); + final completer = Completer(); + 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, + ), + ), + ); + } +} diff --git a/qr_flutter/lib/src/qr_painter.dart b/qr_flutter/lib/src/qr_painter.dart new file mode 100644 index 0000000..e773c57 --- /dev/null +++ b/qr_flutter/lib/src/qr_painter.dart @@ -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 toImage(double size) { + return toPicture(size).toImage(size.toInt(), size.toInt()); + } + + /// Returns the raw QR code image byte data. + Future 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; + } +} diff --git a/qr_flutter/lib/src/qr_versions.dart b/qr_flutter/lib/src/qr_versions.dart new file mode 100644 index 0000000..0d4713f --- /dev/null +++ b/qr_flutter/lib/src/qr_versions.dart @@ -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); +} diff --git a/qr_flutter/lib/src/types.dart b/qr_flutter/lib/src/types.dart new file mode 100644 index 0000000..07a85e9 --- /dev/null +++ b/qr_flutter/lib/src/types.dart @@ -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; + } +} diff --git a/qr_flutter/lib/src/validator.dart b/qr_flutter/lib/src/validator.dart new file mode 100644 index 0000000..12a1eb8 --- /dev/null +++ b/qr_flutter/lib/src/validator.dart @@ -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, +} diff --git a/qr_flutter/pubspec.lock b/qr_flutter/pubspec.lock new file mode 100644 index 0000000..002bb64 --- /dev/null +++ b/qr_flutter/pubspec.lock @@ -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" diff --git a/qr_flutter/pubspec.yaml b/qr_flutter/pubspec.yaml new file mode 100644 index 0000000..f0e2e74 --- /dev/null +++ b/qr_flutter/pubspec.yaml @@ -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 diff --git a/qr_flutter/test/.golden/qr_image_data_module_styled_golden.png b/qr_flutter/test/.golden/qr_image_data_module_styled_golden.png new file mode 100644 index 0000000..d50b737 Binary files /dev/null and b/qr_flutter/test/.golden/qr_image_data_module_styled_golden.png differ diff --git a/qr_flutter/test/.golden/qr_image_eye_data_module_styled_golden.png b/qr_flutter/test/.golden/qr_image_eye_data_module_styled_golden.png new file mode 100644 index 0000000..275496d Binary files /dev/null and b/qr_flutter/test/.golden/qr_image_eye_data_module_styled_golden.png differ diff --git a/qr_flutter/test/.golden/qr_image_eye_styled_golden.png b/qr_flutter/test/.golden/qr_image_eye_styled_golden.png new file mode 100644 index 0000000..867ae26 Binary files /dev/null and b/qr_flutter/test/.golden/qr_image_eye_styled_golden.png differ diff --git a/qr_flutter/test/.golden/qr_image_foreground_colored_golden.png b/qr_flutter/test/.golden/qr_image_foreground_colored_golden.png new file mode 100644 index 0000000..60de4e7 Binary files /dev/null and b/qr_flutter/test/.golden/qr_image_foreground_colored_golden.png differ diff --git a/qr_flutter/test/.golden/qr_image_golden.png b/qr_flutter/test/.golden/qr_image_golden.png new file mode 100644 index 0000000..493c247 Binary files /dev/null and b/qr_flutter/test/.golden/qr_image_golden.png differ diff --git a/qr_flutter/test/.golden/qr_image_logo_golden.png b/qr_flutter/test/.golden/qr_image_logo_golden.png new file mode 100644 index 0000000..8856a1b Binary files /dev/null and b/qr_flutter/test/.golden/qr_image_logo_golden.png differ diff --git a/qr_flutter/test/.golden/qr_painter_golden.png b/qr_flutter/test/.golden/qr_painter_golden.png new file mode 100644 index 0000000..8b1ab8c Binary files /dev/null and b/qr_flutter/test/.golden/qr_painter_golden.png differ diff --git a/qr_flutter/test/.images/logo_yakka.png b/qr_flutter/test/.images/logo_yakka.png new file mode 100644 index 0000000..dabd102 Binary files /dev/null and b/qr_flutter/test/.images/logo_yakka.png differ diff --git a/qr_flutter/test/all_test.dart b/qr_flutter/test/all_test.dart new file mode 100644 index 0000000..58683ca --- /dev/null +++ b/qr_flutter/test/all_test.dart @@ -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); +} diff --git a/qr_flutter/test/image_test.dart b/qr_flutter/test/image_test.dart new file mode 100644 index 0000000..578c1c8 --- /dev/null +++ b/qr_flutter/test/image_test.dart @@ -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')), + ), + ), + ), + ), + ['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 pumpWidgetWithImages( + WidgetTester tester, + Widget widget, + List assetNames, +) async { + Future? precacheFuture; + await tester.pumpWidget( + Builder( + builder: (buildContext) { + precacheFuture = tester.runAsync(() async { + await Future.wait(>[ + 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), + ); +} diff --git a/qr_flutter/test/painter_test.dart b/qr_flutter/test/painter_test.dart new file mode 100644 index 0000000..3e292c9 --- /dev/null +++ b/qr_flutter/test/painter_test.dart @@ -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'), + ); + }); +}