Browse Source

implemented zxing reader and writer

pull/3/head
Khoren Markosyan 3 years ago
parent
commit
37fc595f91
  1. 2
      example/android/app/build.gradle
  2. 26
      example/ios/Podfile.lock
  3. 2
      example/ios/Runner/Info.plist
  4. 40
      example/lib/main.dart
  5. 199
      example/lib/zxing_page.dart
  6. 233
      example/pubspec.lock
  7. 20
      example/pubspec.yaml
  8. 62
      lib/flutter_zxing.dart
  9. 57
      lib/image_converter.dart
  10. 69
      lib/isolate_utils.dart
  11. 158
      lib/scanner_overlay.dart
  12. 238
      lib/zxing_reader_widget.dart
  13. 119
      lib/zxing_writer_widget.dart
  14. 6
      pubspec.yaml

2
example/android/app/build.gradle

@ -44,7 +44,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.markosyan.flutter_zxing_example" applicationId "com.markosyan.flutter_zxing_example"
minSdkVersion 21 minSdkVersion 23
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName

26
example/ios/Podfile.lock

@ -2,27 +2,51 @@ PODS:
- camera (0.0.1): - camera (0.0.1):
- Flutter - Flutter
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_beep (0.0.1):
- Flutter
- flutter_zxing (0.0.1): - flutter_zxing (0.0.1):
- Flutter - Flutter
- path_provider_ios (0.0.1):
- Flutter
- share_plus (0.0.1):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES: DEPENDENCIES:
- camera (from `.symlinks/plugins/camera/ios`) - camera (from `.symlinks/plugins/camera/ios`)
- Flutter (from `Flutter`) - Flutter (from `Flutter`)
- flutter_beep (from `.symlinks/plugins/flutter_beep/ios`)
- flutter_zxing (from `.symlinks/plugins/flutter_zxing/ios`) - flutter_zxing (from `.symlinks/plugins/flutter_zxing/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
EXTERNAL SOURCES: EXTERNAL SOURCES:
camera: camera:
:path: ".symlinks/plugins/camera/ios" :path: ".symlinks/plugins/camera/ios"
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_beep:
:path: ".symlinks/plugins/flutter_beep/ios"
flutter_zxing: flutter_zxing:
:path: ".symlinks/plugins/flutter_zxing/ios" :path: ".symlinks/plugins/flutter_zxing/ios"
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS: SPEC CHECKSUMS:
camera: 9993f92f2c793e87b65e35f3a23c70582afb05b1 camera: 9993f92f2c793e87b65e35f3a23c70582afb05b1
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
flutter_beep: 54fb393b22dfa0f0e4573c81b1c74dd71c4e5af8
flutter_zxing: 19a866d17c8a87ee1026d68521c69d2f008635f6 flutter_zxing: 19a866d17c8a87ee1026d68521c69d2f008635f6
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
COCOAPODS: 1.11.2 COCOAPODS: 1.11.3

2
example/ios/Runner/Info.plist

@ -43,5 +43,7 @@
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>NSCameraUsageDescription</key>
<string>$(APP_DISPLAY_NAME) needs camera access to scan barcodes</string>
</dict> </dict>
</plist> </plist>

40
example/lib/main.dart

@ -1,8 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart'; import 'package:flutter_zxing_example/zxing_page.dart';
import 'package:flutter_zxing/flutter_zxing.dart';
void main() { void main() {
runApp(const MyApp()); runApp(const MyApp());
@ -16,47 +14,15 @@ class MyApp extends StatefulWidget {
} }
class _MyAppState extends State<MyApp> { class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
@override @override
void initState() { void initState() {
super.initState(); super.initState();
initPlatformState();
}
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
String platformVersion;
// Platform messages may fail, so we use a try/catch PlatformException.
// We also handle the message potentially returning null.
try {
platformVersion =
await FlutterZxing.platformVersion ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return const MaterialApp(
home: Scaffold( home: ZxingPage(),
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Text('Running on: $_platformVersion\n'),
),
),
); );
} }
} }

199
example/lib/zxing_page.dart

@ -0,0 +1,199 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_zxing/flutter_zxing.dart';
import 'package:flutter_zxing/generated_bindings.dart';
import 'package:flutter_zxing/zxing_reader_widget.dart';
import 'package:flutter_zxing/zxing_writer_widget.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
late Directory tempDir;
String get tempPath => '${tempDir.path}/zxing.jpg';
class ZxingPage extends StatefulWidget {
const ZxingPage({
Key? key,
}) : super(key: key);
@override
State<ZxingPage> createState() => _ZxingPageState();
}
class _ZxingPageState extends State<ZxingPage> with TickerProviderStateMixin {
CameraController? controller;
TabController? _tabController;
bool isAndroid() => Theme.of(context).platform == TargetPlatform.android;
// Scan result queue
final _resultQueue = <CodeResult>[];
// Write result
Uint8List? writeResult;
// true when the camera is active
bool _isScanning = true;
@override
void initState() {
super.initState();
initStateAsync();
}
void initStateAsync() async {
_tabController = TabController(length: 3, vsync: this);
_tabController?.addListener(() {
_isScanning = _tabController?.index == 0;
if (_isScanning) {
controller?.resumePreview();
} else {
controller?.pausePreview();
}
});
getTemporaryDirectory().then((value) {
tempDir = value;
});
}
@override
void dispose() {
super.dispose();
controller?.dispose();
}
void showInSnackBar(String message) {}
void logError(String code, String? message) {
if (message != null) {
debugPrint('Error: $code\nError Message: $message');
} else {
debugPrint('Error: $code');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Scanner'),
bottom: TabBar(
controller: _tabController,
tabs: const [
Tab(text: 'Scanner'),
Tab(text: 'Result'),
Tab(text: 'Writer'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
// Scanner
ZxingReaderWidget(onScan: (result) async {
_resultQueue.insert(0, result);
_tabController?.index = 1;
await Future.delayed(const Duration(milliseconds: 500));
setState(() {});
}),
// Result
_buildResultList(),
// Writer
SingleChildScrollView(
child: Column(
children: [
ZxingWriterWidget(
onSuccess: (result) {
setState(() {
writeResult = result;
});
},
onError: (error) {
setState(() {
writeResult = null;
});
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
error,
textAlign: TextAlign.center,
),
),
);
},
),
if (writeResult != null) buildWriteResult(),
],
),
),
],
),
);
}
Column buildWriteResult() {
return Column(
children: [
// Barcode image
Image.memory(writeResult ?? Uint8List(0)),
// Share button
ElevatedButton(
onPressed: () {
// Save image to device
final file = File(tempPath);
file.writeAsBytesSync(writeResult ?? Uint8List(0));
final path = file.path;
// Share image
Share.shareFiles([path]);
},
child: const Text('Share'),
),
],
);
}
_buildResultList() {
return _resultQueue.isEmpty
? const Center(
child: Text(
'No Results',
style: TextStyle(fontSize: 24),
))
: ListView.builder(
itemCount: _resultQueue.length,
itemBuilder: (context, index) {
final result = _resultQueue[index];
return ListTile(
title: Text(result.textString),
subtitle: Text(result.formatString),
trailing: ButtonBar(
mainAxisSize: MainAxisSize.min,
children: [
// Copy button
TextButton(
child: const Text('Copy'),
onPressed: () {
Clipboard.setData(
ClipboardData(text: result.textString));
},
),
// Remove button
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () {
_resultQueue.removeAt(index);
setState(() {});
},
),
],
),
);
},
);
}
}

233
example/pubspec.lock

@ -1,6 +1,13 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
archive:
dependency: transitive
description:
name: archive
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.2"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -71,6 +78,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.3.2" version: "0.3.2"
crypto:
dependency: transitive
description:
name: crypto
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -92,11 +106,25 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.2" version: "1.1.2"
file:
dependency: transitive
description:
name: file
url: "https://pub.dartlang.org"
source: hosted
version: "6.1.2"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_beep:
dependency: transitive
description:
name: flutter_beep
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -128,6 +156,13 @@ packages:
relative: true relative: true
source: path source: path
version: "0.0.1" version: "0.0.1"
image:
dependency: transitive
description:
name: image
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.3"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -163,6 +198,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.7.0" version: "1.7.0"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -170,6 +212,69 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.8.0" version: "1.8.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.9"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.12"
path_provider_ios:
dependency: transitive
description:
name: path_provider_ios
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.5"
path_provider_macos:
dependency: transitive
description:
name: path_provider_macos
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
petitparser:
dependency: transitive
description:
name: petitparser
url: "https://pub.dartlang.org"
source: hosted
version: "4.4.0"
platform:
dependency: transitive
description:
name: platform
url: "https://pub.dartlang.org"
source: hosted
version: "3.1.0"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -177,6 +282,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
process:
dependency: transitive
description:
name: process
url: "https://pub.dartlang.org"
source: hosted
version: "4.2.4"
quiver: quiver:
dependency: transitive dependency: transitive
description: description:
@ -184,6 +296,48 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.1+1" version: "3.0.1+1"
share_plus:
dependency: "direct main"
description:
name: share_plus
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.1"
share_plus_linux:
dependency: transitive
description:
name: share_plus_linux
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
share_plus_macos:
dependency: transitive
description:
name: share_plus_macos
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
share_plus_platform_interface:
dependency: transitive
description:
name: share_plus_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
share_plus_web:
dependency: transitive
description:
name: share_plus_web
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
share_plus_windows:
dependency: transitive
description:
name: share_plus_windows
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -245,6 +399,62 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
url_launcher:
dependency: transitive
description:
name: url_launcher
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.20"
url_launcher_android:
dependency: transitive
description:
name: url_launcher_android
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.15"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.15"
url_launcher_linux:
dependency: transitive
description:
name: url_launcher_linux
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
url_launcher_macos:
dependency: transitive
description:
name: url_launcher_macos
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
url_launcher_platform_interface:
dependency: transitive
description:
name: url_launcher_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
url_launcher_web:
dependency: transitive
description:
name: url_launcher_web
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.9"
url_launcher_windows:
dependency: transitive
description:
name: url_launcher_windows
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@ -252,6 +462,27 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
win32:
dependency: transitive
description:
name: win32
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.2"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0+1"
xml:
dependency: transitive
description:
name: xml
url: "https://pub.dartlang.org"
source: hosted
version: "5.3.1"
sdks: sdks:
dart: ">=2.16.1 <3.0.0" dart: ">=2.16.1 <3.0.0"
flutter: ">=2.5.0" flutter: ">=2.10.0"

20
example/pubspec.yaml

@ -1,32 +1,24 @@
name: flutter_zxing_example name: flutter_zxing_example
description: Demonstrates how to use the flutter_zxing plugin. description: Demonstrates how to use the flutter_zxing plugin.
version: 0.0.1
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: "none" # Remove this line if you wish to publish to pub.dev
environment: environment:
sdk: ">=2.16.1 <3.0.0" sdk: ">=2.16.1 <3.0.0"
dependencies: dependencies:
cupertino_icons: ^1.0.2
flutter: flutter:
sdk: flutter sdk: flutter
flutter_zxing: flutter_zxing:
path: ../ path: ../
path_provider: ^2.0.9
cupertino_icons: ^1.0.2 share_plus: ^4.0.1
dev_dependencies: dev_dependencies:
flutter_lints: ^1.0.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^1.0.0
# The following section is specific to Flutter.
flutter: flutter:
uses-material-design: true uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg

62
lib/flutter_zxing.dart

@ -56,3 +56,65 @@ extension Uint8ListBlobConversion on Uint8List {
return blob; return blob;
} }
} }
extension Encode on EncodeResult {
bool get isValidBool => isValid == 1;
Uint32List get bytes => data.asTypedList(length);
String get errorMessage => error.cast<Utf8>().toDartString();
}
extension Code on CodeResult {
bool get isValidBool => isValid == 1;
String get textString => text.cast<Utf8>().toDartString();
String get formatString {
return CodeFormat.formatName(format);
}
}
extension CodeFormat on Format {
static String formatName(int format) => formatNames[format] ?? 'Unknown';
String get name => formatNames[this] ?? 'Unknown';
static final formatNames = {
Format.None: 'None',
Format.Aztec: 'Aztec',
Format.Codabar: 'CodaBar',
Format.Code39: 'Code39',
Format.Code93: 'Code93',
Format.Code128: 'Code128',
Format.DataBar: 'DataBar',
Format.DataBarExpanded: 'DataBarExpanded',
Format.DataMatrix: 'DataMatrix',
Format.EAN8: 'EAN8',
Format.EAN13: 'EAN13',
Format.ITF: 'ITF',
Format.MaxiCode: 'MaxiCode',
Format.PDF417: 'PDF417',
Format.QRCode: 'QR Code',
Format.UPCA: 'UPCA',
Format.UPCE: 'UPCE',
Format.OneDCodes: 'OneD',
Format.TwoDCodes: 'TwoD',
Format.Any: 'Any',
};
static final writerFormats = [
Format.QRCode,
Format.DataMatrix,
Format.Aztec,
Format.PDF417,
Format.Codabar,
Format.Code39,
Format.Code93,
Format.Code128,
Format.EAN8,
Format.EAN13,
Format.ITF,
Format.UPCA,
Format.UPCE,
// Format.DataBar,
// Format.DataBarExpanded,
// Format.MaxiCode,
];
}

57
lib/image_converter.dart

@ -0,0 +1,57 @@
import 'dart:typed_data';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as imglib;
// https://gist.github.com/Alby-o/fe87e35bc21d534c8220aed7df028e03
Future<Uint8List> convertImage(CameraImage image) async {
try {
late imglib.Image img;
if (image.format.group == ImageFormatGroup.yuv420) {
// img = convertYUV420(image);
return image.planes.first.bytes;
} else if (image.format.group == ImageFormatGroup.bgra8888) {
img = convertBGRA8888(image);
}
return img.getBytes(format: imglib.Format.luminance);
} catch (e) {
debugPrint(">>>>>>>>>>>> ERROR:" + e.toString());
}
return Uint8List(0);
}
imglib.Image convertBGRA8888(CameraImage image) {
return imglib.Image.fromBytes(
image.width,
image.height,
image.planes[0].bytes,
format: imglib.Format.bgra,
);
}
// ignore: unused_element
imglib.Image convertYUV420(CameraImage image) {
var img = imglib.Image(image.width, image.height); // Create Image buffer
Plane plane = image.planes[0];
const int shift = (0xFF << 24);
// Fill image buffer with plane[0] from YUV420_888
for (int x = 0; x < image.width; x++) {
for (int planeOffset = 0;
planeOffset < image.height * image.width;
planeOffset += image.width) {
final pixelColor = plane.bytes[planeOffset + x];
// color: 0x FF FF FF FF
// A B G R
// Calculate pixel color
var newVal = shift | (pixelColor << 16) | (pixelColor << 8) | pixelColor;
img.data[planeOffset + x] = newVal;
}
}
return img;
}

69
lib/isolate_utils.dart

@ -0,0 +1,69 @@
import 'dart:isolate';
import 'dart:math';
import 'package:camera/camera.dart';
import 'flutter_zxing.dart';
import 'image_converter.dart';
// Inspired from https://github.com/am15h/object_detection_flutter
/// Bundles data to pass between Isolate
class IsolateData {
CameraImage cameraImage;
double cropPercent;
SendPort? responsePort;
IsolateData(
this.cameraImage,
this.cropPercent,
);
}
/// Manages separate Isolate instance for inference
class IsolateUtils {
static const String kDebugName = "ZxingIsolate";
// ignore: unused_field
Isolate? _isolate;
final _receivePort = ReceivePort();
SendPort? _sendPort;
SendPort? get sendPort => _sendPort;
Future<void> start() async {
_isolate = await Isolate.spawn<SendPort>(
entryPoint,
_receivePort.sendPort,
debugName: kDebugName,
);
_sendPort = await _receivePort.first;
}
void stop() {
_isolate?.kill(priority: Isolate.immediate);
_isolate = null;
_sendPort = null;
}
static void entryPoint(SendPort sendPort) async {
final port = ReceivePort();
sendPort.send(port.sendPort);
await for (final IsolateData? isolateData in port) {
if (isolateData != null) {
final image = isolateData.cameraImage;
final cropPercent = isolateData.cropPercent;
final bytes = await convertImage(image);
final cropSize = (min(image.width, image.height) * cropPercent).round();
final result =
FlutterZxing.zxingRead(bytes, image.width, image.height, cropSize);
isolateData.responsePort?.send(result);
}
}
}
}

158
lib/scanner_overlay.dart

@ -0,0 +1,158 @@
import 'package:flutter/material.dart';
class ScannerOverlay extends ShapeBorder {
const ScannerOverlay({
this.borderColor = Colors.red,
this.borderWidth = 3.0,
this.overlayColor = const Color.fromRGBO(0, 0, 0, 40),
this.borderRadius = 0,
this.borderLength = 40,
this.cutOutSize = 250,
}) : assert(borderLength <= cutOutSize / 2 + borderWidth * 2,
"Border can't be larger than ${cutOutSize / 2 + borderWidth * 2}");
final Color borderColor;
final double borderWidth;
final Color overlayColor;
final double borderRadius;
final double borderLength;
final double cutOutSize;
@override
EdgeInsetsGeometry get dimensions => const EdgeInsets.all(10);
@override
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
return Path()
..fillType = PathFillType.evenOdd
..addPath(getOuterPath(rect), Offset.zero);
}
@override
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
Path _getLeftTopPath(Rect rect) {
return Path()
..moveTo(rect.left, rect.bottom)
..lineTo(rect.left, rect.top)
..lineTo(rect.right, rect.top);
}
return _getLeftTopPath(rect)
..lineTo(
rect.right,
rect.bottom,
)
..lineTo(
rect.left,
rect.bottom,
)
..lineTo(
rect.left,
rect.top,
);
}
@override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
final width = rect.width;
final borderWidthSize = width / 2;
final height = rect.height;
final borderOffset = borderWidth / 2;
final _borderLength = borderLength > cutOutSize / 2 + borderWidth * 2
? borderWidthSize / 2
: borderLength;
final _cutOutSize = cutOutSize < width ? cutOutSize : width - borderOffset;
final backgroundPaint = Paint()
..color = overlayColor
..style = PaintingStyle.fill;
final borderPaint = Paint()
..color = borderColor
..style = PaintingStyle.stroke
..strokeWidth = borderWidth;
final boxPaint = Paint()
..color = borderColor
..style = PaintingStyle.fill
..blendMode = BlendMode.dstOut;
final cutOutRect = Rect.fromLTWH(
rect.left + width / 2 - _cutOutSize / 2 + borderOffset,
rect.top + height / 2 - _cutOutSize / 2 + borderOffset,
_cutOutSize - borderOffset * 2,
_cutOutSize - borderOffset * 2,
);
canvas
..saveLayer(
rect,
backgroundPaint,
)
..drawRect(
rect,
backgroundPaint,
)
// Draw top right corner
..drawRRect(
RRect.fromLTRBAndCorners(
cutOutRect.right - _borderLength,
cutOutRect.top,
cutOutRect.right,
cutOutRect.top + _borderLength,
topRight: Radius.circular(borderRadius),
),
borderPaint,
)
// Draw top left corner
..drawRRect(
RRect.fromLTRBAndCorners(
cutOutRect.left,
cutOutRect.top,
cutOutRect.left + _borderLength,
cutOutRect.top + _borderLength,
topLeft: Radius.circular(borderRadius),
),
borderPaint,
)
// Draw bottom right corner
..drawRRect(
RRect.fromLTRBAndCorners(
cutOutRect.right - _borderLength,
cutOutRect.bottom - _borderLength,
cutOutRect.right,
cutOutRect.bottom,
bottomRight: Radius.circular(borderRadius),
),
borderPaint,
)
// Draw bottom left corner
..drawRRect(
RRect.fromLTRBAndCorners(
cutOutRect.left,
cutOutRect.bottom - _borderLength,
cutOutRect.left + _borderLength,
cutOutRect.bottom,
bottomLeft: Radius.circular(borderRadius),
),
borderPaint,
)
..drawRRect(
RRect.fromRectAndRadius(
cutOutRect,
Radius.circular(borderRadius),
),
boxPaint,
)
..restore();
}
@override
ShapeBorder scale(double t) {
return ScannerOverlay(
borderColor: borderColor,
borderWidth: borderWidth,
overlayColor: overlayColor,
);
}
}

238
lib/zxing_reader_widget.dart

@ -0,0 +1,238 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_beep/flutter_beep.dart';
import 'flutter_zxing.dart';
import 'generated_bindings.dart';
import 'isolate_utils.dart';
import 'scanner_overlay.dart';
class ZxingReaderWidget extends StatefulWidget {
const ZxingReaderWidget({
Key? key,
required this.onScan,
this.onControllerCreated,
this.beep = true,
this.showCroppingRect = true,
this.scanDelay = const Duration(milliseconds: 500), // 500ms delay
this.cropPercent = 0.5, // 50% of the screen
this.resolution = ResolutionPreset.high,
}) : super(key: key);
final Function(CodeResult) onScan;
final Function(CameraController?)? onControllerCreated;
final bool beep;
final bool showCroppingRect;
final Duration scanDelay;
final double cropPercent;
final ResolutionPreset resolution;
@override
State<ZxingReaderWidget> createState() => _ZxingReaderWidgetState();
}
class _ZxingReaderWidgetState extends State<ZxingReaderWidget>
with TickerProviderStateMixin {
late List<CameraDescription> cameras;
CameraController? controller;
bool isAndroid() => Theme.of(context).platform == TargetPlatform.android;
// true when code detecting is ongoing
bool _isProcessing = false;
/// Instance of [IsolateUtils]
IsolateUtils? isolateUtils;
@override
void initState() {
super.initState();
initStateAsync();
}
void initStateAsync() async {
// Spawn a new isolate
isolateUtils = IsolateUtils();
await isolateUtils?.start();
availableCameras().then((cameras) {
setState(() {
this.cameras = cameras;
onNewCameraSelected(cameras.first);
});
});
SystemChannels.lifecycle.setMessageHandler((message) async {
debugPrint(message);
final CameraController? cameraController = controller;
if (cameraController == null || !cameraController.value.isInitialized) {
return;
}
if (mounted) {
if (message == AppLifecycleState.paused.toString()) {
cameraController.dispose();
}
if (message == AppLifecycleState.resumed.toString()) {
onNewCameraSelected(cameraController.description);
}
}
return null;
});
}
@override
void dispose() {
controller?.dispose();
isolateUtils?.stop();
super.dispose();
}
void onNewCameraSelected(CameraDescription cameraDescription) async {
if (controller != null) {
await controller!.dispose();
}
controller = CameraController(
cameraDescription,
widget.resolution,
enableAudio: false,
imageFormatGroup:
isAndroid() ? ImageFormatGroup.yuv420 : ImageFormatGroup.bgra8888,
);
try {
await controller?.initialize();
controller?.startImageStream(processCameraImage);
} on CameraException catch (e) {
_showCameraException(e);
}
controller?.addListener(() {
if (mounted) setState(() {});
});
if (mounted) {
setState(() {});
}
widget.onControllerCreated?.call(controller);
}
void _showCameraException(CameraException e) {
logError(e.code, e.description);
showInSnackBar('Error: ${e.code}\n${e.description}');
}
void showInSnackBar(String message) {}
void logError(String code, String? message) {
if (message != null) {
debugPrint('Error: $code\nError Message: $message');
} else {
debugPrint('Error: $code');
}
}
processCameraImage(CameraImage image) async {
if (!_isProcessing) {
_isProcessing = true;
try {
var isolateData = IsolateData(image, widget.cropPercent);
/// perform inference in separate isolate
CodeResult result = await inference(isolateData);
if (result.isValidBool) {
if (widget.beep) {
FlutterBeep.beep();
}
widget.onScan(result);
setState(() {});
await Future.delayed(const Duration(seconds: 1));
}
} on FileSystemException catch (e) {
debugPrint(e.message);
} catch (e) {
debugPrint(e.toString());
}
await Future.delayed(widget.scanDelay);
_isProcessing = false;
}
return null;
}
/// Runs inference in another isolate
Future<CodeResult> inference(IsolateData isolateData) async {
ReceivePort responsePort = ReceivePort();
isolateUtils?.sendPort
?.send(isolateData..responsePort = responsePort.sendPort);
var results = await responsePort.first;
return results;
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final cropSize = min(size.width, size.height) * widget.cropPercent;
return Stack(
children: [
// Camera preview
Center(child: _cameraPreviewWidget(cropSize)),
],
);
}
// Display the preview from the camera.
Widget _cameraPreviewWidget(double cropSize) {
final CameraController? cameraController = controller;
if (cameraController == null || !cameraController.value.isInitialized) {
return const CircularProgressIndicator();
} else {
final size = MediaQuery.of(context).size;
var cameraMaxSize = max(size.width, size.height);
return Stack(
children: [
SizedBox(
width: cameraMaxSize,
height: cameraMaxSize,
child: ClipRRect(
child: OverflowBox(
alignment: Alignment.center,
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: cameraMaxSize,
child: CameraPreview(
cameraController,
),
),
),
),
),
),
widget.showCroppingRect
? Container(
decoration: ShapeDecoration(
shape: ScannerOverlay(
borderColor: Theme.of(context).primaryColor,
overlayColor: const Color.fromRGBO(0, 0, 0, 0.5),
borderRadius: 1,
borderLength: 16,
borderWidth: 8,
cutOutSize: cropSize,
),
),
)
: Container()
],
);
}
}
}

119
lib/zxing_writer_widget.dart

@ -0,0 +1,119 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:image/image.dart' as imglib;
import 'flutter_zxing.dart';
import 'generated_bindings.dart';
class ZxingWriterWidget extends StatefulWidget {
const ZxingWriterWidget({
Key? key,
this.onSuccess,
this.onError,
}) : super(key: key);
final Function(Uint8List)? onSuccess;
final Function(String)? onError;
@override
State<ZxingWriterWidget> createState() => _ZxingWriterWidgetState();
}
class _ZxingWriterWidgetState extends State<ZxingWriterWidget>
with TickerProviderStateMixin {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _textController = TextEditingController();
bool isAndroid() => Theme.of(context).platform == TargetPlatform.android;
final _maxTextLength = 2000;
final _supportedFormats = CodeFormat.writerFormats;
var _codeFormat = Format.QRCode;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Form(
key: _formKey,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const SizedBox(height: 20),
// Format DropDown button
DropdownButtonFormField<int>(
value: _codeFormat,
items: _supportedFormats
.map((format) => DropdownMenuItem(
value: format,
child: Text(CodeFormat.formatName(format)),
))
.toList(),
onChanged: (format) {
setState(() {
_codeFormat = format ?? Format.QRCode;
});
},
),
const SizedBox(height: 20),
// Input multiline text
TextFormField(
controller: _textController,
keyboardType: TextInputType.multiline,
maxLines: null,
maxLength: _maxTextLength,
onChanged: (value) {
setState(() {});
},
decoration: InputDecoration(
border: const OutlineInputBorder(),
filled: true,
hintText: 'Enter text to encode',
counterText:
'${_textController.value.text.length} / $_maxTextLength',
),
),
// Write button
ElevatedButton(
onPressed: () {
if (_formKey.currentState?.validate() ?? false) {
_formKey.currentState?.save();
FocusScope.of(context).unfocus();
final text = _textController.value.text;
const width = 300;
const height = 300;
final result = FlutterZxing.zxingEncode(
text, width, height, _codeFormat, 5, 0);
String? error;
if (result.isValidBool) {
try {
final img =
imglib.Image.fromBytes(width, height, result.bytes);
final resultBytes =
Uint8List.fromList(imglib.encodeJpg(img));
widget.onSuccess?.call(resultBytes);
} on Exception catch (e) {
error = e.toString();
}
} else {
error = result.errorMessage;
}
if (error != null) {
debugPrint(error);
widget.onError?.call(error);
}
}
},
child: const Text('Encode'),
),
const SizedBox(height: 20),
],
),
),
),
);
}
}

6
pubspec.yaml

@ -11,6 +11,8 @@ dependencies:
ffi: ^1.1.2 ffi: ^1.1.2
flutter: flutter:
sdk: flutter sdk: flutter
flutter_beep: ^1.0.0
image: ^3.1.3
dev_dependencies: dev_dependencies:
ffigen: ^4.1.3 # dart run ffigen ffigen: ^4.1.3 # dart run ffigen
@ -30,7 +32,7 @@ flutter:
ffigen: ffigen:
name: GeneratedBindings name: GeneratedBindings
description: Bindings to `native_verokit.h`. description: Bindings to `native_verokit.h`.
output: 'lib/generated_bindings.dart' output: "lib/generated_bindings.dart"
headers: headers:
entry-points: entry-points:
- 'ios/Classes/src/native_zxing.h' - "ios/Classes/src/native_zxing.h"

Loading…
Cancel
Save