694 lines
23 KiB
Dart
694 lines
23 KiB
Dart
/*
|
|
* 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;
|
|
}
|
|
}
|