diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
index a81c2de..685e442 100644
--- a/example/android/app/build.gradle
+++ b/example/android/app/build.gradle
@@ -44,7 +44,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.markosyan.flutter_zxing_example"
- minSdkVersion 21
+ minSdkVersion 23
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
index 87f9028..b6a2958 100644
--- a/example/ios/Podfile.lock
+++ b/example/ios/Podfile.lock
@@ -2,27 +2,51 @@ PODS:
- camera (0.0.1):
- Flutter
- Flutter (1.0.0)
+ - flutter_beep (0.0.1):
+ - Flutter
- flutter_zxing (0.0.1):
- Flutter
+ - path_provider_ios (0.0.1):
+ - Flutter
+ - share_plus (0.0.1):
+ - Flutter
+ - url_launcher_ios (0.0.1):
+ - Flutter
DEPENDENCIES:
- camera (from `.symlinks/plugins/camera/ios`)
- Flutter (from `Flutter`)
+ - flutter_beep (from `.symlinks/plugins/flutter_beep/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:
camera:
:path: ".symlinks/plugins/camera/ios"
Flutter:
:path: Flutter
+ flutter_beep:
+ :path: ".symlinks/plugins/flutter_beep/ios"
flutter_zxing:
: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:
camera: 9993f92f2c793e87b65e35f3a23c70582afb05b1
Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
+ flutter_beep: 54fb393b22dfa0f0e4573c81b1c74dd71c4e5af8
flutter_zxing: 19a866d17c8a87ee1026d68521c69d2f008635f6
+ path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
+ share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
+ url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
-COCOAPODS: 1.11.2
+COCOAPODS: 1.11.3
diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist
index 38df93f..7d4f7c0 100644
--- a/example/ios/Runner/Info.plist
+++ b/example/ios/Runner/Info.plist
@@ -43,5 +43,7 @@
UIViewControllerBasedStatusBarAppearance
+ NSCameraUsageDescription
+ $(APP_DISPLAY_NAME) needs camera access to scan barcodes
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 3ca34ce..53582a0 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
-import 'dart:async';
-import 'package:flutter/services.dart';
-import 'package:flutter_zxing/flutter_zxing.dart';
+import 'package:flutter_zxing_example/zxing_page.dart';
void main() {
runApp(const MyApp());
@@ -16,47 +14,15 @@ class MyApp extends StatefulWidget {
}
class _MyAppState extends State {
- String _platformVersion = 'Unknown';
-
@override
void initState() {
super.initState();
- initPlatformState();
- }
-
- // Platform messages are asynchronous, so we initialize in an async method.
- Future 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
Widget build(BuildContext context) {
- return MaterialApp(
- home: Scaffold(
- appBar: AppBar(
- title: const Text('Plugin example app'),
- ),
- body: Center(
- child: Text('Running on: $_platformVersion\n'),
- ),
- ),
+ return const MaterialApp(
+ home: ZxingPage(),
);
}
}
diff --git a/example/lib/zxing_page.dart b/example/lib/zxing_page.dart
new file mode 100644
index 0000000..8cc2a67
--- /dev/null
+++ b/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 createState() => _ZxingPageState();
+}
+
+class _ZxingPageState extends State with TickerProviderStateMixin {
+ CameraController? controller;
+ TabController? _tabController;
+
+ bool isAndroid() => Theme.of(context).platform == TargetPlatform.android;
+
+ // Scan result queue
+ final _resultQueue = [];
+
+ // 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(() {});
+ },
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/example/pubspec.lock b/example/pubspec.lock
index fc4b6f4..1fd1c8d 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -1,6 +1,13 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
+ archive:
+ dependency: transitive
+ description:
+ name: archive
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.2.2"
async:
dependency: transitive
description:
@@ -71,6 +78,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.2"
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.0.1"
cupertino_icons:
dependency: "direct main"
description:
@@ -92,11 +106,25 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.2"
+ file:
+ dependency: transitive
+ description:
+ name: file
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "6.1.2"
flutter:
dependency: "direct main"
description: flutter
source: sdk
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:
dependency: "direct dev"
description:
@@ -128,6 +156,13 @@ packages:
relative: true
source: path
version: "0.0.1"
+ image:
+ dependency: transitive
+ description:
+ name: image
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "3.1.3"
js:
dependency: transitive
description:
@@ -163,6 +198,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
+ mime:
+ dependency: transitive
+ description:
+ name: mime
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.1"
path:
dependency: transitive
description:
@@ -170,6 +212,69 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description:
@@ -177,6 +282,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.2"
+ process:
+ dependency: transitive
+ description:
+ name: process
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "4.2.4"
quiver:
dependency: transitive
description:
@@ -184,6 +296,48 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description: flutter
@@ -245,6 +399,62 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dependency: transitive
description:
@@ -252,6 +462,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
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:
dart: ">=2.16.1 <3.0.0"
- flutter: ">=2.5.0"
+ flutter: ">=2.10.0"
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index aff5ff5..41773e4 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -1,32 +1,24 @@
name: flutter_zxing_example
description: Demonstrates how to use the flutter_zxing plugin.
-
-publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+version: 0.0.1
+publish_to: "none" # Remove this line if you wish to publish to pub.dev
environment:
sdk: ">=2.16.1 <3.0.0"
dependencies:
+ cupertino_icons: ^1.0.2
flutter:
sdk: flutter
-
flutter_zxing:
path: ../
-
- cupertino_icons: ^1.0.2
+ path_provider: ^2.0.9
+ share_plus: ^4.0.1
dev_dependencies:
+ flutter_lints: ^1.0.0
flutter_test:
sdk: flutter
- flutter_lints: ^1.0.0
-# The following section is specific to Flutter.
flutter:
-
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
-
diff --git a/lib/flutter_zxing.dart b/lib/flutter_zxing.dart
index dd357c6..72824f4 100644
--- a/lib/flutter_zxing.dart
+++ b/lib/flutter_zxing.dart
@@ -56,3 +56,65 @@ extension Uint8ListBlobConversion on Uint8List {
return blob;
}
}
+
+extension Encode on EncodeResult {
+ bool get isValidBool => isValid == 1;
+ Uint32List get bytes => data.asTypedList(length);
+ String get errorMessage => error.cast().toDartString();
+}
+
+extension Code on CodeResult {
+ bool get isValidBool => isValid == 1;
+ String get textString => text.cast().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,
+ ];
+}
diff --git a/lib/image_converter.dart b/lib/image_converter.dart
new file mode 100644
index 0000000..51e8b88
--- /dev/null
+++ b/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 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;
+}
diff --git a/lib/isolate_utils.dart b/lib/isolate_utils.dart
new file mode 100644
index 0000000..8cbdb21
--- /dev/null
+++ b/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 start() async {
+ _isolate = await Isolate.spawn(
+ 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);
+ }
+ }
+ }
+}
diff --git a/lib/scanner_overlay.dart b/lib/scanner_overlay.dart
new file mode 100644
index 0000000..4eefc29
--- /dev/null
+++ b/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,
+ );
+ }
+}
diff --git a/lib/zxing_reader_widget.dart b/lib/zxing_reader_widget.dart
new file mode 100644
index 0000000..0e4f693
--- /dev/null
+++ b/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 createState() => _ZxingReaderWidgetState();
+}
+
+class _ZxingReaderWidgetState extends State
+ with TickerProviderStateMixin {
+ late List 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 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()
+ ],
+ );
+ }
+ }
+}
diff --git a/lib/zxing_writer_widget.dart b/lib/zxing_writer_widget.dart
new file mode 100644
index 0000000..1523bb7
--- /dev/null
+++ b/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 createState() => _ZxingWriterWidgetState();
+}
+
+class _ZxingWriterWidgetState extends State
+ with TickerProviderStateMixin {
+ final GlobalKey _formKey = GlobalKey();
+ 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(
+ 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),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index eb4559c..60b259e 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -11,6 +11,8 @@ dependencies:
ffi: ^1.1.2
flutter:
sdk: flutter
+ flutter_beep: ^1.0.0
+ image: ^3.1.3
dev_dependencies:
ffigen: ^4.1.3 # dart run ffigen
@@ -30,7 +32,7 @@ flutter:
ffigen:
name: GeneratedBindings
description: Bindings to `native_verokit.h`.
- output: 'lib/generated_bindings.dart'
+ output: "lib/generated_bindings.dart"
headers:
entry-points:
- - 'ios/Classes/src/native_zxing.h'
\ No newline at end of file
+ - "ios/Classes/src/native_zxing.h"