qrpainter.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. /*
  2. * QR.Flutter
  3. * Copyright (c) 2019 the QR.Flutter authors.
  4. * See LICENSE for distribution and usage details.
  5. */
  6. import 'dart:async';
  7. import 'dart:ui' as ui;
  8. import 'package:flutter/services.dart';
  9. import 'package:flutter/widgets.dart';
  10. import 'package:flyinsonolite/infrastructure/scale.dart';
  11. import 'package:qr/qr.dart';
  12. import 'package:flyinsonolite/controls/qrcode/qrtypes.dart';
  13. import 'package:flyinsonolite/controls/qrcode/qrversions.dart';
  14. import 'package:flyinsonolite/controls/qrcode/paintcache.dart';
  15. import 'package:flyinsonolite/controls/qrcode/qrerrors.dart';
  16. import 'package:flyinsonolite/controls/qrcode/qrvalidator.dart';
  17. // ignore_for_file: deprecated_member_use_from_same_package
  18. const _finderPatternLimit = 7;
  19. // default color for the qr code pixels
  20. const Color? _qrDefaultColor = null;
  21. /// A [CustomPainter] object that you can use to paint a QR code.
  22. class QrPainter extends CustomPainter {
  23. /// Create a new QRPainter with passed options (or defaults).
  24. QrPainter({
  25. required String data,
  26. required this.version,
  27. this.errorCorrectionLevel = QrErrorCorrectLevel.L,
  28. this.color = _qrDefaultColor,
  29. this.emptyColor,
  30. this.gapless = false,
  31. this.embeddedImage,
  32. this.embeddedImageStyle,
  33. this.eyeStyle = const QrEyeStyle(
  34. eyeShape: QrEyeShape.square,
  35. color: Color(0xFF000000),
  36. ),
  37. this.dataModuleStyle = const QrDataModuleStyle(
  38. dataModuleShape: QrDataModuleShape.square,
  39. color: Color(0xFF000000),
  40. ),
  41. }) : assert(QrVersions.isSupportedVersion(version)) {
  42. _init(data);
  43. }
  44. /// Create a new QrPainter with a pre-validated/created [QrCode] object. This
  45. /// constructor is useful when you have a custom validation / error handling
  46. /// flow or for when you need to pre-validate the QR data.
  47. QrPainter.withQr({
  48. required QrCode qr,
  49. this.color = _qrDefaultColor,
  50. this.emptyColor,
  51. this.gapless = false,
  52. this.embeddedImage,
  53. this.embeddedImageStyle,
  54. this.eyeStyle = const QrEyeStyle(
  55. eyeShape: QrEyeShape.square,
  56. color: Color(0xFF000000),
  57. ),
  58. this.dataModuleStyle = const QrDataModuleStyle(
  59. dataModuleShape: QrDataModuleShape.square,
  60. color: Color(0xFF000000),
  61. ),
  62. }) : _qr = qr,
  63. version = qr.typeNumber,
  64. errorCorrectionLevel = qr.errorCorrectLevel {
  65. _calcVersion = version;
  66. _initPaints();
  67. }
  68. /// The QR code version.
  69. final int version; // the qr code version
  70. /// The error correction level of the QR code.
  71. final int errorCorrectionLevel; // the qr code error correction level
  72. /// The color of the squares.
  73. @Deprecated('use colors in eyeStyle and dataModuleStyle instead')
  74. final Color? color; // the color of the dark squares
  75. /// The color of the non-squares (background).
  76. @Deprecated(
  77. 'You should use the background color value of your container widget')
  78. final Color? emptyColor; // the other color
  79. /// If set to false, the painter will leave a 1px gap between each of the
  80. /// squares.
  81. final bool gapless;
  82. /// The image data to embed (as an overlay) in the QR code. The image will
  83. /// be added to the center of the QR code.
  84. final ui.Image? embeddedImage;
  85. /// Styling options for the image overlay.
  86. final QrEmbeddedImageStyle? embeddedImageStyle;
  87. /// Styling option for QR Eye ball and frame.
  88. final QrEyeStyle eyeStyle;
  89. /// Styling option for QR data module.
  90. final QrDataModuleStyle dataModuleStyle;
  91. /// The base QR code data
  92. QrCode? _qr;
  93. /// QR Image renderer
  94. late QrImage _qrImage;
  95. /// This is the version (after calculating) that we will use if the user has
  96. /// requested the 'auto' version.
  97. late final int _calcVersion;
  98. /// The size of the 'gap' between the pixels
  99. double get _gapSize => 0.25.s;
  100. /// Cache for all of the [Paint] objects.
  101. final _paintCache = PaintCache();
  102. void _init(String data) {
  103. if (!QrVersions.isSupportedVersion(version)) {
  104. throw QrUnsupportedVersionException(version);
  105. }
  106. // configure and make the QR code data
  107. final validationResult = QrValidator.validate(
  108. data: data,
  109. version: version,
  110. errorCorrectionLevel: errorCorrectionLevel,
  111. );
  112. if (!validationResult.isValid) {
  113. throw validationResult.error!;
  114. }
  115. _qr = validationResult.qrCode;
  116. _calcVersion = _qr!.typeNumber;
  117. _initPaints();
  118. }
  119. void _initPaints() {
  120. // Initialize `QrImage` for rendering
  121. _qrImage = QrImage(_qr!);
  122. // Cache the pixel paint object. For now there is only one but we might
  123. // expand it to multiple later (e.g.: different colours).
  124. _paintCache.cache(
  125. Paint()..style = PaintingStyle.fill, QrCodeElement.codePixel);
  126. // Cache the empty pixel paint object. Empty color is deprecated and will go
  127. // away.
  128. _paintCache.cache(
  129. Paint()..style = PaintingStyle.fill, QrCodeElement.codePixelEmpty);
  130. // Cache the finder pattern painters. We'll keep one for each one in case
  131. // we want to provide customization options later.
  132. for (final position in FinderPatternPosition.values) {
  133. _paintCache.cache(Paint()..style = PaintingStyle.stroke,
  134. QrCodeElement.finderPatternOuter,
  135. position: position);
  136. _paintCache.cache(Paint()..style = PaintingStyle.stroke,
  137. QrCodeElement.finderPatternInner,
  138. position: position);
  139. _paintCache.cache(
  140. Paint()..style = PaintingStyle.fill, QrCodeElement.finderPatternDot,
  141. position: position);
  142. }
  143. }
  144. @override
  145. void paint(Canvas canvas, Size size) {
  146. // if the widget has a zero size side then we cannot continue painting.
  147. if (size.shortestSide == 0) {
  148. print("[QR] WARN: width or height is zero. You should set a 'size' value "
  149. "or nest this painter in a Widget that defines a non-zero size");
  150. return;
  151. }
  152. final paintMetrics = _PaintMetrics(
  153. containerSize: size.shortestSide,
  154. moduleCount: _qr!.moduleCount,
  155. gapSize: (gapless ? 0 : _gapSize),
  156. );
  157. // draw the finder pattern elements
  158. _drawFinderPatternItem(FinderPatternPosition.topLeft, canvas, paintMetrics);
  159. _drawFinderPatternItem(
  160. FinderPatternPosition.bottomLeft, canvas, paintMetrics);
  161. _drawFinderPatternItem(
  162. FinderPatternPosition.topRight, canvas, paintMetrics);
  163. // DEBUG: draw the inner content boundary
  164. // final paint = Paint()..style = ui.PaintingStyle.stroke;
  165. // paint.strokeWidth = 1;
  166. // paint.color = const Color(0x55222222);
  167. // canvas.drawRect(
  168. // Rect.fromLTWH(paintMetrics.inset, paintMetrics.inset,
  169. // paintMetrics.innerContentSize, paintMetrics.innerContentSize),
  170. // paint);
  171. double left;
  172. double top;
  173. final gap = !gapless ? _gapSize : 0;
  174. // get the painters for the pixel information
  175. final pixelPaint = _paintCache.firstPaint(QrCodeElement.codePixel);
  176. if (color != null) {
  177. pixelPaint!.color = color!;
  178. } else {
  179. pixelPaint!.color = dataModuleStyle.color!;
  180. }
  181. Paint? emptyPixelPaint;
  182. if (emptyColor != null) {
  183. emptyPixelPaint = _paintCache.firstPaint(QrCodeElement.codePixelEmpty);
  184. emptyPixelPaint!.color = emptyColor!;
  185. }
  186. for (var x = 0; x < _qr!.moduleCount; x++) {
  187. for (var y = 0; y < _qr!.moduleCount; y++) {
  188. // draw the finder patterns independently
  189. if (_isFinderPatternPosition(x, y)) continue;
  190. final paint = _qrImage.isDark(y, x) ? pixelPaint : emptyPixelPaint;
  191. if (paint == null) continue;
  192. // paint a pixel
  193. left = paintMetrics.inset + (x * (paintMetrics.pixelSize + gap));
  194. top = paintMetrics.inset + (y * (paintMetrics.pixelSize + gap));
  195. var pixelHTweak = 0.0;
  196. var pixelVTweak = 0.0;
  197. if (gapless && _hasAdjacentHorizontalPixel(x, y, _qr!.moduleCount)) {
  198. pixelHTweak = 0.5;
  199. }
  200. if (gapless && _hasAdjacentVerticalPixel(x, y, _qr!.moduleCount)) {
  201. pixelVTweak = 0.5;
  202. }
  203. final squareRect = Rect.fromLTWH(
  204. left,
  205. top,
  206. paintMetrics.pixelSize + pixelHTweak,
  207. paintMetrics.pixelSize + pixelVTweak,
  208. );
  209. if (dataModuleStyle.dataModuleShape == QrDataModuleShape.square) {
  210. canvas.drawRect(squareRect, paint);
  211. } else {
  212. final roundedRect = RRect.fromRectAndRadius(squareRect,
  213. Radius.circular(paintMetrics.pixelSize + pixelHTweak));
  214. canvas.drawRRect(roundedRect, paint);
  215. }
  216. }
  217. }
  218. if (embeddedImage != null) {
  219. final originalSize = Size(
  220. embeddedImage!.width.toDouble(),
  221. embeddedImage!.height.toDouble(),
  222. );
  223. final requestedSize =
  224. embeddedImageStyle != null ? embeddedImageStyle!.size : null;
  225. final imageSize = _scaledAspectSize(size, originalSize, requestedSize);
  226. final position = Offset(
  227. (size.width - imageSize.width) / 2.0,
  228. (size.height - imageSize.height) / 2.0,
  229. );
  230. // draw the image overlay.
  231. _drawImageOverlay(canvas, position, imageSize, embeddedImageStyle);
  232. }
  233. }
  234. bool _hasAdjacentVerticalPixel(int x, int y, int moduleCount) {
  235. if (y + 1 >= moduleCount) return false;
  236. return _qrImage.isDark(y + 1, x);
  237. }
  238. bool _hasAdjacentHorizontalPixel(int x, int y, int moduleCount) {
  239. if (x + 1 >= moduleCount) return false;
  240. return _qrImage.isDark(y, x + 1);
  241. }
  242. bool _isFinderPatternPosition(int x, int y) {
  243. final isTopLeft = (y < _finderPatternLimit && x < _finderPatternLimit);
  244. final isBottomLeft = (y < _finderPatternLimit &&
  245. (x >= _qr!.moduleCount - _finderPatternLimit));
  246. final isTopRight = (y >= _qr!.moduleCount - _finderPatternLimit &&
  247. (x < _finderPatternLimit));
  248. return isTopLeft || isBottomLeft || isTopRight;
  249. }
  250. void _drawFinderPatternItem(
  251. FinderPatternPosition position,
  252. Canvas canvas,
  253. _PaintMetrics metrics,
  254. ) {
  255. final totalGap = (_finderPatternLimit - 1) * metrics.gapSize;
  256. final radius = ((_finderPatternLimit * metrics.pixelSize) + totalGap) -
  257. metrics.pixelSize;
  258. final strokeAdjust = (metrics.pixelSize / 2.0);
  259. final edgePos =
  260. (metrics.inset + metrics.innerContentSize) - (radius + strokeAdjust);
  261. Offset offset;
  262. if (position == FinderPatternPosition.topLeft) {
  263. offset =
  264. Offset(metrics.inset + strokeAdjust, metrics.inset + strokeAdjust);
  265. } else if (position == FinderPatternPosition.bottomLeft) {
  266. offset = Offset(metrics.inset + strokeAdjust, edgePos);
  267. } else {
  268. offset = Offset(edgePos, metrics.inset + strokeAdjust);
  269. }
  270. // configure the paints
  271. final outerPaint = _paintCache.firstPaint(QrCodeElement.finderPatternOuter,
  272. position: position)!;
  273. outerPaint.strokeWidth = metrics.pixelSize;
  274. if (color != null) {
  275. outerPaint.color = color!;
  276. } else {
  277. outerPaint.color = eyeStyle.color!;
  278. }
  279. final innerPaint = _paintCache.firstPaint(QrCodeElement.finderPatternInner,
  280. position: position)!;
  281. innerPaint.strokeWidth = metrics.pixelSize;
  282. innerPaint.color = emptyColor ?? const Color(0x00ffffff);
  283. final dotPaint = _paintCache.firstPaint(QrCodeElement.finderPatternDot,
  284. position: position);
  285. if (color != null) {
  286. dotPaint!.color = color!;
  287. } else {
  288. dotPaint!.color = eyeStyle.color!;
  289. }
  290. final outerRect = Rect.fromLTWH(offset.dx, offset.dy, radius, radius);
  291. final innerRadius = radius - (2 * metrics.pixelSize);
  292. final innerRect = Rect.fromLTWH(offset.dx + metrics.pixelSize,
  293. offset.dy + metrics.pixelSize, innerRadius, innerRadius);
  294. final gap = metrics.pixelSize * 2;
  295. final dotSize = radius - gap - (2 * strokeAdjust);
  296. final dotRect = Rect.fromLTWH(offset.dx + metrics.pixelSize + strokeAdjust,
  297. offset.dy + metrics.pixelSize + strokeAdjust, dotSize, dotSize);
  298. if (eyeStyle.eyeShape == QrEyeShape.square) {
  299. canvas.drawRect(outerRect, outerPaint);
  300. canvas.drawRect(innerRect, innerPaint);
  301. canvas.drawRect(dotRect, dotPaint);
  302. } else {
  303. final roundedOuterStrokeRect =
  304. RRect.fromRectAndRadius(outerRect, Radius.circular(radius));
  305. canvas.drawRRect(roundedOuterStrokeRect, outerPaint);
  306. final roundedInnerStrokeRect =
  307. RRect.fromRectAndRadius(outerRect, Radius.circular(innerRadius));
  308. canvas.drawRRect(roundedInnerStrokeRect, innerPaint);
  309. final roundedDotStrokeRect =
  310. RRect.fromRectAndRadius(dotRect, Radius.circular(dotSize));
  311. canvas.drawRRect(roundedDotStrokeRect, dotPaint);
  312. }
  313. }
  314. bool _hasOneNonZeroSide(Size size) => size.longestSide > 0;
  315. Size _scaledAspectSize(
  316. Size widgetSize, Size originalSize, Size? requestedSize) {
  317. if (requestedSize != null && !requestedSize.isEmpty) {
  318. return requestedSize;
  319. } else if (requestedSize != null && _hasOneNonZeroSide(requestedSize)) {
  320. final maxSide = requestedSize.longestSide;
  321. final ratio = maxSide / originalSize.longestSide;
  322. return Size(ratio * originalSize.width, ratio * originalSize.height);
  323. } else {
  324. final maxSide = 0.25 * widgetSize.shortestSide;
  325. final ratio = maxSide / originalSize.longestSide;
  326. return Size(ratio * originalSize.width, ratio * originalSize.height);
  327. }
  328. }
  329. void _drawImageOverlay(
  330. Canvas canvas, Offset position, Size size, QrEmbeddedImageStyle? style) {
  331. final paint = Paint()
  332. ..isAntiAlias = true
  333. ..filterQuality = FilterQuality.high;
  334. if (style != null) {
  335. if (style.color != null) {
  336. paint.colorFilter = ColorFilter.mode(style.color!, BlendMode.srcATop);
  337. }
  338. }
  339. final srcSize =
  340. Size(embeddedImage!.width.toDouble(), embeddedImage!.height.toDouble());
  341. final src = Alignment.center.inscribe(srcSize, Offset.zero & srcSize);
  342. final dst = Alignment.center.inscribe(size, position & size);
  343. canvas.drawImageRect(embeddedImage!, src, dst, paint);
  344. }
  345. @override
  346. bool shouldRepaint(CustomPainter oldPainter) {
  347. if (oldPainter is QrPainter) {
  348. return errorCorrectionLevel != oldPainter.errorCorrectionLevel ||
  349. _calcVersion != oldPainter._calcVersion ||
  350. _qr != oldPainter._qr ||
  351. gapless != oldPainter.gapless ||
  352. embeddedImage != oldPainter.embeddedImage ||
  353. embeddedImageStyle != oldPainter.embeddedImageStyle ||
  354. eyeStyle != oldPainter.eyeStyle ||
  355. dataModuleStyle != oldPainter.dataModuleStyle;
  356. }
  357. return true;
  358. }
  359. /// Returns a [ui.Picture] object containing the QR code data.
  360. ui.Picture toPicture(double size) {
  361. final recorder = ui.PictureRecorder();
  362. final canvas = Canvas(recorder);
  363. paint(canvas, Size(size, size));
  364. return recorder.endRecording();
  365. }
  366. /// Returns the raw QR code [ui.Image] object.
  367. Future<ui.Image> toImage(double size,
  368. {ui.ImageByteFormat format = ui.ImageByteFormat.png}) async {
  369. return await toPicture(size).toImage(size.toInt(), size.toInt());
  370. }
  371. /// Returns the raw QR code image byte data.
  372. Future<ByteData?> toImageData(double size,
  373. {ui.ImageByteFormat format = ui.ImageByteFormat.png}) async {
  374. final image = await toImage(size, format: format);
  375. return image.toByteData(format: format);
  376. }
  377. }
  378. class _PaintMetrics {
  379. _PaintMetrics(
  380. {required this.containerSize,
  381. required this.gapSize,
  382. required this.moduleCount}) {
  383. _calculateMetrics();
  384. }
  385. final int moduleCount;
  386. final double containerSize;
  387. final double gapSize;
  388. late final double _pixelSize;
  389. double get pixelSize => _pixelSize;
  390. late final double _innerContentSize;
  391. double get innerContentSize => _innerContentSize;
  392. late final double _inset;
  393. double get inset => _inset;
  394. void _calculateMetrics() {
  395. final gapTotal = (moduleCount - 1) * gapSize;
  396. var pixelSize = (containerSize - gapTotal) / moduleCount;
  397. _pixelSize = (pixelSize * 2).roundToDouble() / 2;
  398. _innerContentSize = (_pixelSize * moduleCount) + gapTotal;
  399. _inset = (containerSize - _innerContentSize) / 2;
  400. }
  401. }