Skip to content

Commit 5f4ba4d

Browse files
committedJan 18, 2024
feat: added text-recognizing camera to the app
1 parent 98133bb commit 5f4ba4d

10 files changed

+850
-3
lines changed
 

‎.flutter-plugins-dependencies

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"camera_avfoundation","path":"/home/rave/.pub-cache/hosted/pub.dev/camera_avfoundation-0.9.13+5/","native_build":true,"dependencies":[]},{"name":"image_picker_ios","path":"/home/rave/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.8+2/","native_build":true,"dependencies":[]},{"name":"isar_flutter_libs","path":"/home/rave/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0+1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/home/rave/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"camera_android","path":"/home/rave/.pub-cache/hosted/pub.dev/camera_android-0.10.8+12/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"flutter_plugin_android_lifecycle","path":"/home/rave/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.16/","native_build":true,"dependencies":[]},{"name":"image_picker_android","path":"/home/rave/.pub-cache/hosted/pub.dev/image_picker_android-0.8.8+1/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"isar_flutter_libs","path":"/home/rave/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0+1/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/home/rave/.pub-cache/hosted/pub.dev/path_provider_android-2.2.0/","native_build":true,"dependencies":[]}],"macos":[{"name":"file_selector_macos","path":"/home/rave/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.3+3/","native_build":true,"dependencies":[]},{"name":"image_picker_macos","path":"/home/rave/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+1/","native_build":false,"dependencies":["file_selector_macos"]},{"name":"isar_flutter_libs","path":"/home/rave/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0+1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/home/rave/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"file_selector_linux","path":"/home/rave/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.2+1/","native_build":true,"dependencies":[]},{"name":"image_picker_linux","path":"/home/rave/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+1/","native_build":false,"dependencies":["file_selector_linux"]},{"name":"isar_flutter_libs","path":"/home/rave/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0+1/","native_build":true,"dependencies":[]},{"name":"path_provider_linux","path":"/home/rave/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"file_selector_windows","path":"/home/rave/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+1/","native_build":true,"dependencies":[]},{"name":"image_picker_windows","path":"/home/rave/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/","native_build":false,"dependencies":["file_selector_windows"]},{"name":"isar_flutter_libs","path":"/home/rave/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0+1/","native_build":true,"dependencies":[]},{"name":"path_provider_windows","path":"/home/rave/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/","native_build":false,"dependencies":[]}],"web":[{"name":"camera_web","path":"/home/rave/.pub-cache/hosted/pub.dev/camera_web-0.3.2+3/","dependencies":[]},{"name":"image_picker_for_web","path":"/home/rave/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.1/","dependencies":[]}]},"dependencyGraph":[{"name":"camera","dependencies":["camera_android","camera_avfoundation","camera_web","flutter_plugin_android_lifecycle"]},{"name":"camera_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"camera_avfoundation","dependencies":[]},{"name":"camera_web","dependencies":[]},{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios","image_picker_linux","image_picker_macos","image_picker_windows"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]},{"name":"image_picker_linux","dependencies":["file_selector_linux"]},{"name":"image_picker_macos","dependencies":["file_selector_macos"]},{"name":"image_picker_windows","dependencies":["file_selector_windows"]},{"name":"isar_flutter_libs","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]}],"date_created":"2024-01-13 10:51:38.816330","version":"3.16.5"}
1+
{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"camera_avfoundation","path":"/home/rave/.pub-cache/hosted/pub.dev/camera_avfoundation-0.9.13+5/","native_build":true,"dependencies":[]},{"name":"google_mlkit_commons","path":"/home/rave/.pub-cache/hosted/pub.dev/google_mlkit_commons-0.6.1/","native_build":true,"dependencies":[]},{"name":"google_mlkit_text_recognition","path":"/home/rave/.pub-cache/hosted/pub.dev/google_mlkit_text_recognition-0.11.0/","native_build":true,"dependencies":["google_mlkit_commons"]},{"name":"image_picker_ios","path":"/home/rave/.pub-cache/hosted/pub.dev/image_picker_ios-0.8.8+2/","native_build":true,"dependencies":[]},{"name":"isar_flutter_libs","path":"/home/rave/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0+1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/home/rave/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"camera_android","path":"/home/rave/.pub-cache/hosted/pub.dev/camera_android-0.10.8+12/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"flutter_plugin_android_lifecycle","path":"/home/rave/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.16/","native_build":true,"dependencies":[]},{"name":"google_mlkit_commons","path":"/home/rave/.pub-cache/hosted/pub.dev/google_mlkit_commons-0.6.1/","native_build":true,"dependencies":[]},{"name":"google_mlkit_text_recognition","path":"/home/rave/.pub-cache/hosted/pub.dev/google_mlkit_text_recognition-0.11.0/","native_build":true,"dependencies":["google_mlkit_commons"]},{"name":"image_picker_android","path":"/home/rave/.pub-cache/hosted/pub.dev/image_picker_android-0.8.8+1/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"]},{"name":"isar_flutter_libs","path":"/home/rave/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0+1/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/home/rave/.pub-cache/hosted/pub.dev/path_provider_android-2.2.0/","native_build":true,"dependencies":[]}],"macos":[{"name":"file_selector_macos","path":"/home/rave/.pub-cache/hosted/pub.dev/file_selector_macos-0.9.3+3/","native_build":true,"dependencies":[]},{"name":"image_picker_macos","path":"/home/rave/.pub-cache/hosted/pub.dev/image_picker_macos-0.2.1+1/","native_build":false,"dependencies":["file_selector_macos"]},{"name":"isar_flutter_libs","path":"/home/rave/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0+1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/home/rave/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.1/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"file_selector_linux","path":"/home/rave/.pub-cache/hosted/pub.dev/file_selector_linux-0.9.2+1/","native_build":true,"dependencies":[]},{"name":"image_picker_linux","path":"/home/rave/.pub-cache/hosted/pub.dev/image_picker_linux-0.2.1+1/","native_build":false,"dependencies":["file_selector_linux"]},{"name":"isar_flutter_libs","path":"/home/rave/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0+1/","native_build":true,"dependencies":[]},{"name":"path_provider_linux","path":"/home/rave/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]}],"windows":[{"name":"file_selector_windows","path":"/home/rave/.pub-cache/hosted/pub.dev/file_selector_windows-0.9.3+1/","native_build":true,"dependencies":[]},{"name":"image_picker_windows","path":"/home/rave/.pub-cache/hosted/pub.dev/image_picker_windows-0.2.1+1/","native_build":false,"dependencies":["file_selector_windows"]},{"name":"isar_flutter_libs","path":"/home/rave/.pub-cache/hosted/pub.dev/isar_flutter_libs-3.1.0+1/","native_build":true,"dependencies":[]},{"name":"path_provider_windows","path":"/home/rave/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/","native_build":false,"dependencies":[]}],"web":[{"name":"camera_web","path":"/home/rave/.pub-cache/hosted/pub.dev/camera_web-0.3.2+3/","dependencies":[]},{"name":"image_picker_for_web","path":"/home/rave/.pub-cache/hosted/pub.dev/image_picker_for_web-3.0.1/","dependencies":[]}]},"dependencyGraph":[{"name":"camera","dependencies":["camera_android","camera_avfoundation","camera_web","flutter_plugin_android_lifecycle"]},{"name":"camera_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"camera_avfoundation","dependencies":[]},{"name":"camera_web","dependencies":[]},{"name":"file_selector_linux","dependencies":[]},{"name":"file_selector_macos","dependencies":[]},{"name":"file_selector_windows","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"google_mlkit_commons","dependencies":[]},{"name":"google_mlkit_text_recognition","dependencies":["google_mlkit_commons"]},{"name":"image_picker","dependencies":["image_picker_android","image_picker_for_web","image_picker_ios","image_picker_linux","image_picker_macos","image_picker_windows"]},{"name":"image_picker_android","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"image_picker_for_web","dependencies":[]},{"name":"image_picker_ios","dependencies":[]},{"name":"image_picker_linux","dependencies":["file_selector_linux"]},{"name":"image_picker_macos","dependencies":["file_selector_macos"]},{"name":"image_picker_windows","dependencies":["file_selector_windows"]},{"name":"isar_flutter_libs","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]}],"date_created":"2024-01-18 18:50:22.435711","version":"3.16.5"}

‎lib/Pages/BudgetCap/budget_cap.dart

+13-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:allassabudget/Pages/AddExpenseManually/categories_grid_selectabl
22
import 'package:allassabudget/Pages/AddExpenseManually/categories_selecting_controller.dart';
33
import 'package:flutter/material.dart';
44
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
5+
import 'package:allassabudget/utils.dart';
56

67
class BudgetCap extends StatelessWidget {
78
BudgetCap({super.key});
@@ -13,7 +14,18 @@ class BudgetCap extends StatelessWidget {
1314
}
1415
showDialog(context: context, builder: (context) {
1516
return AlertDialog(
16-
title: Text(),
17+
title: Text(AppLocalizations.of(context)!.confirmCapCategories),
18+
content: Column(
19+
children: categoriesController
20+
.categoriesList.map((category) => Row(children: [
21+
Icon(Icons.circle, color: associatedColor(category.name),),
22+
Text(category.name)
23+
],)).toList(),
24+
),
25+
actions: [
26+
TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(AppLocalizations.of(context)!.cancel)),
27+
ElevatedButton(onPressed: () {}, child: Text(AppLocalizations.of(context)!.confirm))
28+
],
1729
);
1830
});
1931
}

‎lib/Pages/Homepage/homepage.dart

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:allassabudget/Storage/Models/expense.dart';
2+
import 'package:allassabudget/TextRecognition/text_detector_view.dart';
23
import 'package:allassabudget/logger.dart';
34
import 'package:allassabudget/Storage/storage.dart';
45
import 'package:flutter/material.dart';
@@ -16,6 +17,7 @@ class HomePage extends StatelessWidget {
1617

1718
void pickImage() async {
1819
logger.info("Opening camera");
20+
1921
XFile? newImage = await _imagePicker.pickImage(source: ImageSource.camera);
2022

2123
if(newImage == null) {
@@ -35,7 +37,7 @@ class HomePage extends StatelessWidget {
3537
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
3638
children: [
3739
MaterialButton(
38-
onPressed: pickImage,
40+
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (context) => new TextRecognizerView())),
3941
child: Icon(Icons.add_circle, color: Theme.of(context).primaryColor, size: 300,),),
4042
Row(
4143
mainAxisAlignment: MainAxisAlignment.spaceEvenly,

‎lib/TextRecognition/camera_view.dart

+385
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,385 @@
1+
import 'dart:io';
2+
3+
import 'package:camera/camera.dart';
4+
import 'package:flutter/material.dart';
5+
import 'package:flutter/services.dart';
6+
import 'package:google_mlkit_commons/google_mlkit_commons.dart';
7+
8+
class CameraView extends StatefulWidget {
9+
CameraView(
10+
{Key? key,
11+
required this.customPaint,
12+
required this.onImage,
13+
this.onCameraFeedReady,
14+
this.onDetectorViewModeChanged,
15+
this.onCameraLensDirectionChanged,
16+
this.initialCameraLensDirection = CameraLensDirection.back})
17+
: super(key: key);
18+
19+
final CustomPaint? customPaint;
20+
final Function(InputImage inputImage) onImage;
21+
final VoidCallback? onCameraFeedReady;
22+
final VoidCallback? onDetectorViewModeChanged;
23+
final Function(CameraLensDirection direction)? onCameraLensDirectionChanged;
24+
final CameraLensDirection initialCameraLensDirection;
25+
26+
@override
27+
State<CameraView> createState() => _CameraViewState();
28+
}
29+
30+
class _CameraViewState extends State<CameraView> {
31+
static List<CameraDescription> _cameras = [];
32+
CameraController? _controller;
33+
int _cameraIndex = -1;
34+
double _currentZoomLevel = 1.0;
35+
double _minAvailableZoom = 1.0;
36+
double _maxAvailableZoom = 1.0;
37+
double _minAvailableExposureOffset = 0.0;
38+
double _maxAvailableExposureOffset = 0.0;
39+
double _currentExposureOffset = 0.0;
40+
bool _changingCameraLens = false;
41+
42+
@override
43+
void initState() {
44+
super.initState();
45+
46+
_initialize();
47+
}
48+
49+
void _initialize() async {
50+
if (_cameras.isEmpty) {
51+
_cameras = await availableCameras();
52+
}
53+
for (var i = 0; i < _cameras.length; i++) {
54+
if (_cameras[i].lensDirection == widget.initialCameraLensDirection) {
55+
_cameraIndex = i;
56+
break;
57+
}
58+
}
59+
if (_cameraIndex != -1) {
60+
_startLiveFeed();
61+
}
62+
}
63+
64+
@override
65+
void dispose() {
66+
_stopLiveFeed();
67+
super.dispose();
68+
}
69+
70+
@override
71+
Widget build(BuildContext context) {
72+
return Scaffold(body: _liveFeedBody());
73+
}
74+
75+
Widget _liveFeedBody() {
76+
if (_cameras.isEmpty) return Container();
77+
if (_controller == null) return Container();
78+
if (_controller?.value.isInitialized == false) return Container();
79+
return Container(
80+
color: Colors.black,
81+
child: Stack(
82+
fit: StackFit.expand,
83+
children: <Widget>[
84+
Center(
85+
child: _changingCameraLens
86+
? Center(
87+
child: const Text('Changing camera lens'),
88+
)
89+
: CameraPreview(
90+
_controller!,
91+
child: widget.customPaint,
92+
),
93+
),
94+
_backButton(),
95+
_switchLiveCameraToggle(),
96+
_detectionViewModeToggle(),
97+
_zoomControl(),
98+
_exposureControl(),
99+
],
100+
),
101+
);
102+
}
103+
104+
Widget _backButton() => Positioned(
105+
top: 40,
106+
left: 8,
107+
child: SizedBox(
108+
height: 50.0,
109+
width: 50.0,
110+
child: FloatingActionButton(
111+
heroTag: Object(),
112+
onPressed: () => Navigator.of(context).pop(),
113+
backgroundColor: Colors.black54,
114+
child: Icon(
115+
Icons.arrow_back_ios_outlined,
116+
size: 20,
117+
),
118+
),
119+
),
120+
);
121+
122+
Widget _detectionViewModeToggle() => Positioned(
123+
bottom: 8,
124+
left: 8,
125+
child: SizedBox(
126+
height: 50.0,
127+
width: 50.0,
128+
child: FloatingActionButton(
129+
heroTag: Object(),
130+
onPressed: widget.onDetectorViewModeChanged,
131+
backgroundColor: Colors.black54,
132+
child: Icon(
133+
Icons.photo_library_outlined,
134+
size: 25,
135+
),
136+
),
137+
),
138+
);
139+
140+
Widget _switchLiveCameraToggle() => Positioned(
141+
bottom: 8,
142+
right: 8,
143+
child: SizedBox(
144+
height: 50.0,
145+
width: 50.0,
146+
child: FloatingActionButton(
147+
heroTag: Object(),
148+
onPressed: _switchLiveCamera,
149+
backgroundColor: Colors.black54,
150+
child: Icon(
151+
Platform.isIOS
152+
? Icons.flip_camera_ios_outlined
153+
: Icons.flip_camera_android_outlined,
154+
size: 25,
155+
),
156+
),
157+
),
158+
);
159+
160+
Widget _zoomControl() => Positioned(
161+
bottom: 16,
162+
left: 0,
163+
right: 0,
164+
child: Align(
165+
alignment: Alignment.bottomCenter,
166+
child: SizedBox(
167+
width: 250,
168+
child: Row(
169+
mainAxisAlignment: MainAxisAlignment.center,
170+
crossAxisAlignment: CrossAxisAlignment.center,
171+
children: [
172+
Expanded(
173+
child: Slider(
174+
value: _currentZoomLevel,
175+
min: _minAvailableZoom,
176+
max: _maxAvailableZoom,
177+
activeColor: Colors.white,
178+
inactiveColor: Colors.white30,
179+
onChanged: (value) async {
180+
setState(() {
181+
_currentZoomLevel = value;
182+
});
183+
await _controller?.setZoomLevel(value);
184+
},
185+
),
186+
),
187+
Container(
188+
width: 50,
189+
decoration: BoxDecoration(
190+
color: Colors.black54,
191+
borderRadius: BorderRadius.circular(10.0),
192+
),
193+
child: Padding(
194+
padding: const EdgeInsets.all(8.0),
195+
child: Center(
196+
child: Text(
197+
'${_currentZoomLevel.toStringAsFixed(1)}x',
198+
style: TextStyle(color: Colors.white),
199+
),
200+
),
201+
),
202+
),
203+
],
204+
),
205+
),
206+
),
207+
);
208+
209+
Widget _exposureControl() => Positioned(
210+
top: 40,
211+
right: 8,
212+
child: ConstrainedBox(
213+
constraints: BoxConstraints(
214+
maxHeight: 250,
215+
),
216+
child: Column(children: [
217+
Container(
218+
width: 55,
219+
decoration: BoxDecoration(
220+
color: Colors.black54,
221+
borderRadius: BorderRadius.circular(10.0),
222+
),
223+
child: Padding(
224+
padding: const EdgeInsets.all(8.0),
225+
child: Center(
226+
child: Text(
227+
'${_currentExposureOffset.toStringAsFixed(1)}x',
228+
style: TextStyle(color: Colors.white),
229+
),
230+
),
231+
),
232+
),
233+
Expanded(
234+
child: RotatedBox(
235+
quarterTurns: 3,
236+
child: SizedBox(
237+
height: 30,
238+
child: Slider(
239+
value: _currentExposureOffset,
240+
min: _minAvailableExposureOffset,
241+
max: _maxAvailableExposureOffset,
242+
activeColor: Colors.white,
243+
inactiveColor: Colors.white30,
244+
onChanged: (value) async {
245+
setState(() {
246+
_currentExposureOffset = value;
247+
});
248+
await _controller?.setExposureOffset(value);
249+
},
250+
),
251+
),
252+
),
253+
)
254+
]),
255+
),
256+
);
257+
258+
Future _startLiveFeed() async {
259+
final camera = _cameras[_cameraIndex];
260+
_controller = CameraController(
261+
camera,
262+
// Set to ResolutionPreset.high. Do NOT set it to ResolutionPreset.max because for some phones does NOT work.
263+
ResolutionPreset.high,
264+
enableAudio: false,
265+
imageFormatGroup: Platform.isAndroid
266+
? ImageFormatGroup.nv21
267+
: ImageFormatGroup.bgra8888,
268+
);
269+
_controller?.initialize().then((_) {
270+
if (!mounted) {
271+
return;
272+
}
273+
_controller?.getMinZoomLevel().then((value) {
274+
_currentZoomLevel = value;
275+
_minAvailableZoom = value;
276+
});
277+
_controller?.getMaxZoomLevel().then((value) {
278+
_maxAvailableZoom = value;
279+
});
280+
_currentExposureOffset = 0.0;
281+
_controller?.getMinExposureOffset().then((value) {
282+
_minAvailableExposureOffset = value;
283+
});
284+
_controller?.getMaxExposureOffset().then((value) {
285+
_maxAvailableExposureOffset = value;
286+
});
287+
_controller?.startImageStream(_processCameraImage).then((value) {
288+
if (widget.onCameraFeedReady != null) {
289+
widget.onCameraFeedReady!();
290+
}
291+
if (widget.onCameraLensDirectionChanged != null) {
292+
widget.onCameraLensDirectionChanged!(camera.lensDirection);
293+
}
294+
});
295+
setState(() {});
296+
});
297+
}
298+
299+
Future _stopLiveFeed() async {
300+
await _controller?.stopImageStream();
301+
await _controller?.dispose();
302+
_controller = null;
303+
}
304+
305+
Future _switchLiveCamera() async {
306+
setState(() => _changingCameraLens = true);
307+
_cameraIndex = (_cameraIndex + 1) % _cameras.length;
308+
309+
await _stopLiveFeed();
310+
await _startLiveFeed();
311+
setState(() => _changingCameraLens = false);
312+
}
313+
314+
void _processCameraImage(CameraImage image) {
315+
final inputImage = _inputImageFromCameraImage(image);
316+
if (inputImage == null) return;
317+
widget.onImage(inputImage);
318+
}
319+
320+
final _orientations = {
321+
DeviceOrientation.portraitUp: 0,
322+
DeviceOrientation.landscapeLeft: 90,
323+
DeviceOrientation.portraitDown: 180,
324+
DeviceOrientation.landscapeRight: 270,
325+
};
326+
327+
InputImage? _inputImageFromCameraImage(CameraImage image) {
328+
if (_controller == null) return null;
329+
330+
// get image rotation
331+
// it is used in android to convert the InputImage from Dart to Java: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/google_mlkit_commons/android/src/main/java/com/google_mlkit_commons/InputImageConverter.java
332+
// `rotation` is not used in iOS to convert the InputImage from Dart to Obj-C: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/google_mlkit_commons/ios/Classes/MLKVisionImage%2BFlutterPlugin.m
333+
// in both platforms `rotation` and `camera.lensDirection` can be used to compensate `x` and `y` coordinates on a canvas: https://github.com/flutter-ml/google_ml_kit_flutter/blob/master/packages/example/lib/vision_detector_views/painters/coordinates_translator.dart
334+
final camera = _cameras[_cameraIndex];
335+
final sensorOrientation = camera.sensorOrientation;
336+
// print(
337+
// 'lensDirection: ${camera.lensDirection}, sensorOrientation: $sensorOrientation, ${_controller?.value.deviceOrientation} ${_controller?.value.lockedCaptureOrientation} ${_controller?.value.isCaptureOrientationLocked}');
338+
InputImageRotation? rotation;
339+
if (Platform.isIOS) {
340+
rotation = InputImageRotationValue.fromRawValue(sensorOrientation);
341+
} else if (Platform.isAndroid) {
342+
var rotationCompensation =
343+
_orientations[_controller!.value.deviceOrientation];
344+
if (rotationCompensation == null) return null;
345+
if (camera.lensDirection == CameraLensDirection.front) {
346+
// front-facing
347+
rotationCompensation = (sensorOrientation + rotationCompensation) % 360;
348+
} else {
349+
// back-facing
350+
rotationCompensation =
351+
(sensorOrientation - rotationCompensation + 360) % 360;
352+
}
353+
rotation = InputImageRotationValue.fromRawValue(rotationCompensation);
354+
// print('rotationCompensation: $rotationCompensation');
355+
}
356+
if (rotation == null) return null;
357+
// print('final rotation: $rotation');
358+
359+
// get image format
360+
final format = InputImageFormatValue.fromRawValue(image.format.raw);
361+
// validate format depending on platform
362+
// only supported formats:
363+
// * nv21 for Android
364+
// * bgra8888 for iOS
365+
if (format == null ||
366+
(Platform.isAndroid && format != InputImageFormat.nv21) ||
367+
(Platform.isIOS && format != InputImageFormat.bgra8888)) return null;
368+
369+
// since format is constraint to nv21 or bgra8888, both only have one plane
370+
if (image.planes.length != 1) return null;
371+
final plane = image.planes.first;
372+
373+
// compose InputImage using bytes
374+
return InputImage.fromBytes(
375+
bytes: plane.bytes,
376+
metadata: InputImageMetadata(
377+
size: Size(image.width.toDouble(), image.height.toDouble()),
378+
rotation: rotation, // used only in Android
379+
format: format, // used only in iOS
380+
bytesPerRow: plane.bytesPerRow, // used only in iOS
381+
),
382+
);
383+
}
384+
}
385+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import 'dart:io';
2+
import 'dart:ui';
3+
4+
import 'package:camera/camera.dart';
5+
import 'package:google_mlkit_commons/google_mlkit_commons.dart';
6+
7+
double translateX(
8+
double x,
9+
Size canvasSize,
10+
Size imageSize,
11+
InputImageRotation rotation,
12+
CameraLensDirection cameraLensDirection,
13+
) {
14+
switch (rotation) {
15+
case InputImageRotation.rotation90deg:
16+
return x *
17+
canvasSize.width /
18+
(Platform.isIOS ? imageSize.width : imageSize.height);
19+
case InputImageRotation.rotation270deg:
20+
return canvasSize.width -
21+
x *
22+
canvasSize.width /
23+
(Platform.isIOS ? imageSize.width : imageSize.height);
24+
case InputImageRotation.rotation0deg:
25+
case InputImageRotation.rotation180deg:
26+
switch (cameraLensDirection) {
27+
case CameraLensDirection.back:
28+
return x * canvasSize.width / imageSize.width;
29+
default:
30+
return canvasSize.width - x * canvasSize.width / imageSize.width;
31+
}
32+
}
33+
}
34+
35+
double translateY(
36+
double y,
37+
Size canvasSize,
38+
Size imageSize,
39+
InputImageRotation rotation,
40+
CameraLensDirection cameraLensDirection,
41+
) {
42+
switch (rotation) {
43+
case InputImageRotation.rotation90deg:
44+
case InputImageRotation.rotation270deg:
45+
return y *
46+
canvasSize.height /
47+
(Platform.isIOS ? imageSize.height : imageSize.width);
48+
case InputImageRotation.rotation0deg:
49+
case InputImageRotation.rotation180deg:
50+
return y * canvasSize.height / imageSize.height;
51+
}
52+
}
53+
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import 'package:camera/camera.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:google_mlkit_commons/google_mlkit_commons.dart';
4+
5+
import 'camera_view.dart';
6+
7+
enum DetectorViewMode { liveFeed, gallery }
8+
9+
class DetectorView extends StatefulWidget {
10+
DetectorView({
11+
Key? key,
12+
required this.title,
13+
required this.onImage,
14+
this.customPaint,
15+
this.text,
16+
this.initialDetectionMode = DetectorViewMode.liveFeed,
17+
this.initialCameraLensDirection = CameraLensDirection.back,
18+
this.onCameraFeedReady,
19+
this.onDetectorViewModeChanged,
20+
this.onCameraLensDirectionChanged,
21+
}) : super(key: key);
22+
23+
final String title;
24+
final CustomPaint? customPaint;
25+
final String? text;
26+
final DetectorViewMode initialDetectionMode;
27+
final Function(InputImage inputImage) onImage;
28+
final Function()? onCameraFeedReady;
29+
final Function(DetectorViewMode mode)? onDetectorViewModeChanged;
30+
final Function(CameraLensDirection direction)? onCameraLensDirectionChanged;
31+
final CameraLensDirection initialCameraLensDirection;
32+
33+
@override
34+
State<DetectorView> createState() => _DetectorViewState();
35+
}
36+
37+
class _DetectorViewState extends State<DetectorView> {
38+
late DetectorViewMode _mode;
39+
40+
@override
41+
void initState() {
42+
_mode = widget.initialDetectionMode;
43+
super.initState();
44+
}
45+
46+
@override
47+
Widget build(BuildContext context) {
48+
return
49+
CameraView(
50+
customPaint: widget.customPaint,
51+
onImage: widget.onImage,
52+
onCameraFeedReady: widget.onCameraFeedReady,
53+
onDetectorViewModeChanged: _onDetectorViewModeChanged,
54+
initialCameraLensDirection: widget.initialCameraLensDirection,
55+
onCameraLensDirectionChanged: widget.onCameraLensDirectionChanged,
56+
);
57+
}
58+
59+
void _onDetectorViewModeChanged() {
60+
if (_mode == DetectorViewMode.liveFeed) {
61+
_mode = DetectorViewMode.gallery;
62+
} else {
63+
_mode = DetectorViewMode.liveFeed;
64+
}
65+
if (widget.onDetectorViewModeChanged != null) {
66+
widget.onDetectorViewModeChanged!(_mode);
67+
}
68+
setState(() {});
69+
}
70+
}
71+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import 'dart:io';
2+
import 'dart:ui';
3+
import 'dart:ui' as ui;
4+
5+
import 'package:camera/camera.dart';
6+
import 'package:flutter/material.dart';
7+
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
8+
9+
import 'coordinates_translator.dart';
10+
11+
class TextRecognizerPainter extends CustomPainter {
12+
TextRecognizerPainter(
13+
this.recognizedText,
14+
this.imageSize,
15+
this.rotation,
16+
this.cameraLensDirection,
17+
);
18+
19+
final RecognizedText recognizedText;
20+
final Size imageSize;
21+
final InputImageRotation rotation;
22+
final CameraLensDirection cameraLensDirection;
23+
24+
@override
25+
void paint(Canvas canvas, Size size) {
26+
final Paint paint = Paint()
27+
..style = PaintingStyle.stroke
28+
..strokeWidth = 3.0
29+
..color = Colors.lightGreenAccent;
30+
31+
final Paint background = Paint()..color = Color(0x99000000);
32+
33+
for (final textBlock in recognizedText.blocks) {
34+
final ParagraphBuilder builder = ParagraphBuilder(
35+
ParagraphStyle(
36+
textAlign: TextAlign.left,
37+
fontSize: 16,
38+
textDirection: TextDirection.ltr),
39+
);
40+
builder.pushStyle(
41+
ui.TextStyle(color: Colors.lightGreenAccent, background: background));
42+
builder.addText(textBlock.text);
43+
builder.pop();
44+
45+
final left = translateX(
46+
textBlock.boundingBox.left,
47+
size,
48+
imageSize,
49+
rotation,
50+
cameraLensDirection,
51+
);
52+
final top = translateY(
53+
textBlock.boundingBox.top,
54+
size,
55+
imageSize,
56+
rotation,
57+
cameraLensDirection,
58+
);
59+
final right = translateX(
60+
textBlock.boundingBox.right,
61+
size,
62+
imageSize,
63+
rotation,
64+
cameraLensDirection,
65+
);
66+
// final bottom = translateY(
67+
// textBlock.boundingBox.bottom,
68+
// size,
69+
// imageSize,
70+
// rotation,
71+
// cameraLensDirection,
72+
// );
73+
//
74+
// canvas.drawRect(
75+
// Rect.fromLTRB(left, top, right, bottom),
76+
// paint,
77+
// );
78+
79+
final List<Offset> cornerPoints = <Offset>[];
80+
for (final point in textBlock.cornerPoints) {
81+
double x = translateX(
82+
point.x.toDouble(),
83+
size,
84+
imageSize,
85+
rotation,
86+
cameraLensDirection,
87+
);
88+
double y = translateY(
89+
point.y.toDouble(),
90+
size,
91+
imageSize,
92+
rotation,
93+
cameraLensDirection,
94+
);
95+
96+
if (Platform.isAndroid) {
97+
switch (cameraLensDirection) {
98+
case CameraLensDirection.front:
99+
switch (rotation) {
100+
case InputImageRotation.rotation0deg:
101+
case InputImageRotation.rotation90deg:
102+
break;
103+
case InputImageRotation.rotation180deg:
104+
x = size.width - x;
105+
y = size.height - y;
106+
break;
107+
case InputImageRotation.rotation270deg:
108+
x = translateX(
109+
point.y.toDouble(),
110+
size,
111+
imageSize,
112+
rotation,
113+
cameraLensDirection,
114+
);
115+
y = size.height -
116+
translateY(
117+
point.x.toDouble(),
118+
size,
119+
imageSize,
120+
rotation,
121+
cameraLensDirection,
122+
);
123+
break;
124+
}
125+
break;
126+
case CameraLensDirection.back:
127+
switch (rotation) {
128+
case InputImageRotation.rotation0deg:
129+
case InputImageRotation.rotation270deg:
130+
break;
131+
case InputImageRotation.rotation180deg:
132+
x = size.width - x;
133+
y = size.height - y;
134+
break;
135+
case InputImageRotation.rotation90deg:
136+
x = size.width -
137+
translateX(
138+
point.y.toDouble(),
139+
size,
140+
imageSize,
141+
rotation,
142+
cameraLensDirection,
143+
);
144+
y = translateY(
145+
point.x.toDouble(),
146+
size,
147+
imageSize,
148+
rotation,
149+
cameraLensDirection,
150+
);
151+
break;
152+
}
153+
break;
154+
case CameraLensDirection.external:
155+
break;
156+
}
157+
}
158+
159+
cornerPoints.add(Offset(x, y));
160+
}
161+
162+
// Add the first point to close the polygon
163+
cornerPoints.add(cornerPoints.first);
164+
canvas.drawPoints(PointMode.polygon, cornerPoints, paint);
165+
166+
canvas.drawParagraph(
167+
builder.build()
168+
..layout(ParagraphConstraints(
169+
width: (right - left).abs(),
170+
)),
171+
Offset(
172+
Platform.isAndroid &&
173+
cameraLensDirection == CameraLensDirection.front
174+
? right
175+
: left,
176+
top),
177+
);
178+
}
179+
}
180+
181+
@override
182+
bool shouldRepaint(TextRecognizerPainter oldDelegate) {
183+
return oldDelegate.recognizedText != recognizedText;
184+
}
185+
}
186+
+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import 'package:camera/camera.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart';
4+
5+
import 'detector_view.dart';
6+
import 'text_detector_painter.dart';
7+
8+
class TextRecognizerView extends StatefulWidget {
9+
TextRecognizerView({super.key});
10+
11+
@override
12+
State<TextRecognizerView> createState() => _TextRecognizerViewState();
13+
}
14+
15+
class _TextRecognizerViewState extends State<TextRecognizerView> {
16+
var _script = TextRecognitionScript.latin;
17+
var _textRecognizer = TextRecognizer(script: TextRecognitionScript.latin);
18+
bool _canProcess = true;
19+
bool _isBusy = false;
20+
CustomPaint? _customPaint;
21+
String? _text;
22+
var _cameraLensDirection = CameraLensDirection.back;
23+
24+
@override
25+
void dispose() async {
26+
_canProcess = false;
27+
_textRecognizer.close();
28+
super.dispose();
29+
}
30+
31+
@override
32+
Widget build(BuildContext context) {
33+
return Scaffold(
34+
body: Stack(children: [
35+
DetectorView(
36+
title: 'Text Detector',
37+
customPaint: _customPaint,
38+
text: _text,
39+
onImage: _processImage,
40+
initialCameraLensDirection: _cameraLensDirection,
41+
onCameraLensDirectionChanged: (value) => _cameraLensDirection = value,
42+
),
43+
Positioned(
44+
top: 30,
45+
left: 100,
46+
right: 100,
47+
child: Row(
48+
children: [
49+
Spacer(),
50+
Container(
51+
decoration: BoxDecoration(
52+
color: Colors.black54,
53+
borderRadius: BorderRadius.circular(10.0),
54+
),
55+
child: Padding(
56+
padding: const EdgeInsets.all(4.0),
57+
child: _buildDropdown(),
58+
)),
59+
Spacer(),
60+
],
61+
)),
62+
]),
63+
);
64+
}
65+
66+
Widget _buildDropdown() => DropdownButton<TextRecognitionScript>(
67+
value: _script,
68+
icon: const Icon(Icons.arrow_downward),
69+
elevation: 16,
70+
style: const TextStyle(color: Colors.blue),
71+
underline: Container(
72+
height: 2,
73+
color: Colors.blue,
74+
),
75+
onChanged: (TextRecognitionScript? script) {
76+
if (script != null) {
77+
setState(() {
78+
_script = script;
79+
_textRecognizer.close();
80+
_textRecognizer = TextRecognizer(script: _script);
81+
});
82+
}
83+
},
84+
items: TextRecognitionScript.values
85+
.map<DropdownMenuItem<TextRecognitionScript>>((script) {
86+
return DropdownMenuItem<TextRecognitionScript>(
87+
value: script,
88+
child: Text(script.name),
89+
);
90+
}).toList(),
91+
);
92+
93+
Future<void> _processImage(InputImage inputImage) async {
94+
if (!_canProcess) return;
95+
if (_isBusy) return;
96+
_isBusy = true;
97+
setState(() {
98+
_text = '';
99+
});
100+
final recognizedText = await _textRecognizer.processImage(inputImage);
101+
if (inputImage.metadata?.size != null &&
102+
inputImage.metadata?.rotation != null) {
103+
final painter = TextRecognizerPainter(
104+
recognizedText,
105+
inputImage.metadata!.size,
106+
inputImage.metadata!.rotation,
107+
_cameraLensDirection,
108+
);
109+
_customPaint = CustomPaint(painter: painter);
110+
} else {
111+
_text = 'Recognized text:\n\n${recognizedText.text}';
112+
// TODO: set _customPaint to draw boundingRect on top of image
113+
_customPaint = null;
114+
}
115+
_isBusy = false;
116+
if (mounted) {
117+
setState(() {});
118+
}
119+
}
120+
}
121+

‎pubspec.lock

+16
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,22 @@ packages:
349349
url: "https://pub.dev"
350350
source: hosted
351351
version: "2.1.2"
352+
google_mlkit_commons:
353+
dependency: transitive
354+
description:
355+
name: google_mlkit_commons
356+
sha256: "046586b381cdd139f7f6a05ad6998f7e339d061bd70158249907358394b5f496"
357+
url: "https://pub.dev"
358+
source: hosted
359+
version: "0.6.1"
360+
google_mlkit_text_recognition:
361+
dependency: "direct main"
362+
description:
363+
name: google_mlkit_text_recognition
364+
sha256: d484de2a10961a6f0ff8b54cc92b71bfbb0e65509be0903edca0e1f9256ca4c2
365+
url: "https://pub.dev"
366+
source: hosted
367+
version: "0.11.0"
352368
graphs:
353369
dependency: transitive
354370
description:

‎pubspec.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ dependencies:
4242
path: ^1.8.3
4343
path_provider: ^2.1.1
4444
pie_chart: ^5.3.2
45+
google_mlkit_text_recognition: ^0.11.0
4546

4647
dev_dependencies:
4748
flutter_test:

0 commit comments

Comments
 (0)
Please sign in to comment.