diff --git a/.metadata b/.metadata index 3e6e02a..ba5723a 100644 --- a/.metadata +++ b/.metadata @@ -15,21 +15,6 @@ migration: - platform: root create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - - platform: android - create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - - platform: ios - create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - - platform: linux - create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - - platform: macos - create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - - platform: web - create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - platform: windows create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 diff --git a/.vscode/launch.json b/.vscode/launch.json index e46adca..423f8cc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -23,20 +23,6 @@ "request": "launch", "type": "dart", "flutterMode": "release" - }, - { - "name": "Flutter Web (Chrome)", - "request": "launch", - "type": "dart", - "program": "lib/main.dart", - "args": [ - "--web-port", "8080" - ], - "flutterMode": "debug", - "disableWebSecurity": true, - "webRenderer": "auto", - "deviceName": "Chrome", - "args2": [] } ] } \ No newline at end of file diff --git a/README.md b/README.md index 4b27b02..813401a 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,12 @@ # optictext -Ein Tool, das dem Benutzer die Option gibt, OCR auf Bildern anzuwenden oder Text in Bildern zu übersetzen und den übersetzten Text wieder auf das Bild zu setzen. +Leider habe ich es nicht hinbekommen meine requests zu mocken. +Deshalb habe ich nur UI Elemente getestet. -Screen1: Wahl zwischen Image tools und Audio Tools(falls ich in der Zukunft weiter daran Arbeiten werde). - - Image tools screen: DoOCR und ImageTranslate. - DoOCR: User kann ein Bild hochladen und OCR darauf anwenden. - ImageTranslate: User kann ein Bild hochladen welches dann übersetzt wird. - - -Den user die Sprache wählen zu lassen und dann in eine gewählte sprache zu übersetzen ist leicht gemacht, -allerding ist es mir wichtig, das die spracherkennung auch automatisch funktioniert. -Im ersten schritt war es wichtig diese funktionalität zu haben. -Aktuell läuft das auf einem vps, dafür ist der http request da. -Wenn möglich implementiere ich die langauge classfication in dart. - -aktuell teste ich auf einem android phone, es soll aber am ende auch auf IOS laufen. +Falls ich den Code vom Server noch nachreichen soll, kann ich dies machen. +Flutter build apk wirft eine Fehlermeldung aber funktioniert. Bei der Präsentation hatte +ich die release version dabei, die nicht mit "debug" gekennzeichnet war. ## Getting Started diff --git a/android/app/build.gradle b/android/app/build.gradle index dfa4389..a142587 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -45,7 +45,7 @@ android { applicationId "com.example.optictext" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 23de1e6..b9e6958 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ + _BottomBarState(); -} - -class _BottomBarState extends State { - int _selectedIndex = 0; - - // ignore: unused_field - static const List _widgetOptions = [ - Text('Home Page'), - Text('todo'), - Text('pro'), - ]; - - void _onItemTapped(int index) { - setState(() { - _selectedIndex = index; - }); - } - - @override - Widget build(BuildContext context) { - return BottomNavigationBar( - items: const [ - BottomNavigationBarItem( - icon: Icon(Icons.home), - label: 'Home', - ), - BottomNavigationBarItem( - icon: Icon(Icons.business), - label: 'todo', - ), - BottomNavigationBarItem( - icon: Icon(Icons.school), - label: 'pro', - ), - ], - currentIndex: _selectedIndex, - selectedItemColor: Colors.amber[800], - onTap: _onItemTapped, - ); - } -} diff --git a/lib/file_utils.dart b/lib/file_utils.dart new file mode 100644 index 0000000..c4eb59c --- /dev/null +++ b/lib/file_utils.dart @@ -0,0 +1,48 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'dart:typed_data'; + +class FileUtils { + static void saveTranslatedImage(Uint8List translatedImage) async { + if (Platform.isAndroid) { + final result = await ImageGallerySaver.saveImage(translatedImage); + if (result['isSuccess']) { + Fluttertoast.showToast( + msg: "Image saved successfully!", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Colors.green, + textColor: Colors.white, + fontSize: 16.0, + ); + } else { + Fluttertoast.showToast( + msg: "Failed to save image", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); + } + } else { + //vorerst... finde noch was besseres + final file = File('C:/Users/hfggvcb/Desktop/image.png'); + await file.writeAsBytes(translatedImage); + Fluttertoast.showToast( + msg: "Image saved successfully on Windows!", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Colors.green, + textColor: Colors.white, + fontSize: 16.0, + ); + } + } +} diff --git a/lib/http_utils.dart b/lib/http_utils.dart index 78ff06a..a6bcdc7 100644 --- a/lib/http_utils.dart +++ b/lib/http_utils.dart @@ -1,14 +1,21 @@ import 'dart:convert'; +import 'dart:io'; +import 'package:cpd_app/language_utils.dart'; import 'package:flutter/services.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:http/http.dart' as http; +import 'package:http/http.dart'; import 'package:http_parser/http_parser.dart'; +import 'package:flutter/material.dart'; +import 'package:connectivity/connectivity.dart'; +import 'dart:async'; class HttpUtils { var client = http.Client(); - Future checkLang(Uint8List imageBytes, String imageName) async { - var client = http.Client(); - var postUri = Uri.parse("http://130.61.88.150/upload"); + Future checkLang(Uint8List imageBytes, String imageName) async { + var postUri = Uri.parse("http://130.61.27.201/upload"); + http.MultipartRequest request = http.MultipartRequest("POST", postUri); http.MultipartFile multipartFile = http.MultipartFile.fromBytes( @@ -19,6 +26,7 @@ class HttpUtils { ); request.files.add(multipartFile); + http.StreamedResponse response = await client.send(request); http.Response finalResponse = await http.Response.fromStream(response); var lang = ""; @@ -30,4 +38,151 @@ class HttpUtils { } return lang; } + + Future extractTextKnownSource( + Uint8List imageBytes, String sourceLang) async { + var postUri = Uri.parse("http://130.61.27.201/extractWithKnownSource"); + + http.MultipartRequest request = http.MultipartRequest("POST", postUri); + http.MultipartFile multipartFile = http.MultipartFile.fromBytes( + 'file', + imageBytes, + filename: 'lorem.png', + contentType: MediaType('image', 'png'), + ); + + MultipartFile source = MultipartFile.fromString( + 'sourceLang', + sourceLang, + ); + + request.files.add(multipartFile); + request.files.add(source); + + http.StreamedResponse response = await client.send(request); + http.Response finalResponse = await http.Response.fromStream(response); + var lang = ""; + if (finalResponse.statusCode == 200) { + lang = finalResponse.body.toString(); + } else { + throw ('Error: ${finalResponse.statusCode}'); + } + return lang; + } + + Future extractTextUnknownSource(Uint8List imageBytes) async { + var postUri = Uri.parse("http://130.61.27.201/extractWithoutKnownSource"); + + http.MultipartRequest request = http.MultipartRequest("POST", postUri); + + http.MultipartFile multipartFile = http.MultipartFile.fromBytes( + 'file', + imageBytes, + filename: 'lorem.png', + contentType: MediaType('image', 'png'), + ); + + request.files.add(multipartFile); + + http.StreamedResponse response = await client.send(request); + http.Response finalResponse = await http.Response.fromStream(response); + var lang = ""; + if (finalResponse.statusCode == 200) { + lang = finalResponse.body.toString(); + } else { + throw ('Error: ${finalResponse.statusCode}'); + } + return lang; + } + + Future translateKnownSource( + Uint8List imageBytes, String sourceLang, String targetLang) async { + var url = Uri.parse("http://130.61.27.201/translKnownSource"); + + http.MultipartRequest request = http.MultipartRequest("POST", url); + http.MultipartFile multipartFile = http.MultipartFile.fromBytes( + 'file', + imageBytes, + filename: 'lorem.png', + contentType: MediaType('image', 'png'), + ); + + MultipartFile source = MultipartFile.fromString( + 'sourceLang', + LanguageUtils.translatorLanguages[sourceLang]!, + ); + + MultipartFile target = MultipartFile.fromString( + 'targetLang', + LanguageUtils.translatorLanguages[targetLang]!, + ); + + request.files.add(multipartFile); + request.files.add(source); + request.files.add(target); + http.StreamedResponse response = await client.send(request); + http.Response finalResponse = await http.Response.fromStream(response); + Uint8List lang = Uint8List(0); + if (finalResponse.statusCode == 200) { + lang = Uint8List.fromList(finalResponse.bodyBytes); + } else { + throw ('Error: ${finalResponse.statusCode}'); + } + return lang; + } + + void showProgressBar(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return const Dialog( + backgroundColor: Color.fromARGB(0, 44, 44, 44), + elevation: 0, + child: Center( + child: CircularProgressIndicator(), + ), + ); + }, + ); + } + + void hideProgressBar(BuildContext context) { + Navigator.pop(context); + } + + Future internetConnectivityCheck() async { + if (Platform.isAndroid) { + var connectivityResult = await Connectivity().checkConnectivity(); + return connectivityResult == ConnectivityResult.wifi || + connectivityResult == ConnectivityResult.mobile; + } else { + final response = await http.get(Uri.parse('http://130.61.27.201')); + return response.statusCode == 200; + } + } + + void triggerNoInetToast() { + Fluttertoast.showToast( + msg: "No internet connection", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: const Color.fromARGB(255, 161, 120, 17), + textColor: Colors.white, + fontSize: 16.0, + ); + } + + void triggerConnectedToast() { + Fluttertoast.showToast( + msg: "Connected", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: const Color.fromARGB(255, 161, 120, 17), + textColor: Colors.white, + fontSize: 16.0, + ); + } } diff --git a/lib/image_translator.dart b/lib/image_translator.dart index d7a3c05..047bb42 100644 --- a/lib/image_translator.dart +++ b/lib/image_translator.dart @@ -1,10 +1,11 @@ import 'dart:io'; +import 'package:cpd_app/file_utils.dart'; import 'package:cpd_app/http_utils.dart'; -import 'package:http/http.dart' as http; -import 'package:cpd_app/image_uploader.dart'; +import 'package:cpd_app/session_list.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'language_utils.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:image_picker/image_picker.dart'; class ImageTranslation extends StatefulWidget { @@ -16,64 +17,49 @@ class ImageTranslation extends StatefulWidget { class _ImageTranslationState extends State { XFile? _selectedImage; - String _sourceLanguage = 'Auto'; - String _targetLanguage = 'English'; - String _translatedText = ''; - final ImageUploader _imageUploader = ImageUploader(); + bool _wasTranslated = false; + String _sourceLanguage = 'English'; + String _targetLanguage = 'German'; + Uint8List translatedIamge = Uint8List(0); final HttpUtils _httpUtils = HttpUtils(); - /*1. Text extracten - 2. Text übersetzen - 3. Text aus dem alten bild mit farbe überdecken (koordinaten etwas weiter links oben + rechts unten als die erkannten koordinaten) - 4. Übersetzen Text in neues bild einfügen - */ + + Uint8List buildNewImage( + Uint8List imageBytes, String text, String sourceLang, String targetLang) { + return imageBytes; + } + Future uploadImage( Uint8List imageBytes, String sourceLang, String targetLang, ) async { - setState(() { - _translatedText = ''; - }); - - //preprocessing - String text = ''; - if (LanguageUtils.tessLanguages[sourceLang] == 'auto') { - String lang = await _httpUtils.checkLang(imageBytes, 'image.jpg'); - text = await _imageUploader.performOcr(imageBytes, 'image.jpg', lang); - sourceLang = LanguageUtils.tessLanguages[sourceLang]!; - } else { - String langCode = LanguageUtils.tessLanguages[sourceLang]!; - text = await _imageUploader.performOcr(imageBytes, 'image.jpg', langCode); + Uint8List newImageBytes = Uint8List(0); + _httpUtils.showProgressBar(context); + try { + newImageBytes = await _httpUtils.translateKnownSource( + imageBytes, sourceLang, targetLang); + } finally { + // ignore: use_build_context_synchronously + _httpUtils.hideProgressBar(context); } - if (text != '') { - Map requestBody = { - 'source_language': sourceLang, - 'target_language': LanguageUtils.translatorLanguages[targetLang]!, - 'text': text, - }; - - Map headers = { - 'content-type': 'application/x-www-form-urlencoded', - 'X-RapidAPI-Key': 'd0fa3c2f3cmsh1805e7a2fed7cc2p1683ebjsna722e2bffafe', - 'X-RapidAPI-Host': 'text-translator2.p.rapidapi.com', - }; - - final response = await http.post( - Uri.parse('https://text-translator2.p.rapidapi.com/translate'), - headers: headers, - body: requestBody, + if (newImageBytes.isEmpty) { + Fluttertoast.showToast( + msg: "Image could not be translated", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, ); - - if (response.statusCode == 200) { - _translatedText = response.body; - setState(() { - _translatedText = text; - _selectedImage = null; - }); - } else { - print('Error: ${response.statusCode}'); - } - print(_translatedText); + } else { + DateTime now = DateTime.now(); + int milliseconds = now.millisecondsSinceEpoch; + ImageList.addImage(milliseconds.toString(), newImageBytes); + _wasTranslated = true; + setState(() { + translatedIamge = newImageBytes; + }); } } @@ -84,8 +70,22 @@ class _ImageTranslationState extends State { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color.fromARGB(99, 78, 72, 72), appBar: AppBar( + backgroundColor: const Color.fromARGB(255, 161, 120, 17), title: const Text('Image Translation'), + actions: [ + IconButton( + icon: const Icon(Icons.wifi), + onPressed: () async { + if (!await _httpUtils.internetConnectivityCheck()) { + _httpUtils.triggerNoInetToast(); + } else { + _httpUtils.triggerConnectedToast(); + } + }, + ), + ], ), body: Center( child: Column( @@ -95,7 +95,6 @@ class _ImageTranslationState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - // From - Subtle blue background Container( decoration: BoxDecoration( color: Colors.blue.shade100, @@ -114,6 +113,7 @@ class _ImageTranslationState extends State { }); }, items: sourceLanguages + .where((value) => value != 'Auto') .map>((String value) { return DropdownMenuItem( value: value, @@ -124,7 +124,6 @@ class _ImageTranslationState extends State { ], ), ), - // Target - Subtle red background Container( decoration: BoxDecoration( color: Colors.red.shade100, @@ -143,6 +142,7 @@ class _ImageTranslationState extends State { }); }, items: targetLanguages + .where((value) => value != 'Auto') .map>((String value) { return DropdownMenuItem( value: value, @@ -158,22 +158,29 @@ class _ImageTranslationState extends State { const SizedBox(height: 20), TextButton( onPressed: () async { - final ImagePicker picker = ImagePicker(); - final XFile? image = - await picker.pickImage(source: ImageSource.gallery); - if (image != null) { - setState(() { - _selectedImage = image; - }); + if (!_wasTranslated) { + final ImagePicker picker = ImagePicker(); + final XFile? image = + await picker.pickImage(source: ImageSource.gallery); + if (image != null) { + setState(() { + _selectedImage = image; + }); + } + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (BuildContext context) => + const ImageTranslation(), + ), + ); } }, - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.blue), - ), - child: const Text( - 'Upload Image', - style: TextStyle(color: Colors.white), - ), + style: ElevatedButton.styleFrom( + foregroundColor: const Color.fromARGB(255, 0, 0, 0), + backgroundColor: const Color.fromARGB(255, 161, 120, 17)), + child: Text(_wasTranslated ? 'Try Another' : 'Select Image'), ), ElevatedButton( onPressed: _selectedImage != null @@ -186,30 +193,29 @@ class _ImageTranslationState extends State { } } : null, - child: const Text('Extract Text'), - ), - _translatedText.isNotEmpty - ? Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: TextFormField( - readOnly: true, - initialValue: _translatedText, - maxLines: null, - decoration: const InputDecoration( - labelText: 'Extracted Text', - border: OutlineInputBorder(), - ), - ), - ), - ) - : Container(), - TextButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: _translatedText)); - }, - child: const Text('Copy to Clipboard'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 161, 120, 17), + foregroundColor: const Color.fromARGB(255, 0, 0, 0)), + child: const Text('Translate Image'), ), + if (translatedIamge.isEmpty && _selectedImage != null) + Image.file(File(_selectedImage!.path)), + if (translatedIamge.isNotEmpty) Image.memory(translatedIamge), + if (translatedIamge.isNotEmpty) + Visibility( + visible: translatedIamge.isNotEmpty, + child: ElevatedButton( + onPressed: () { + if (translatedIamge.isNotEmpty) { + FileUtils.saveTranslatedImage(translatedIamge); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 161, 120, 17), + foregroundColor: const Color.fromARGB(255, 0, 0, 0)), + child: const Text('Save Translated Image'), + ), + ), ], ), ), diff --git a/lib/language_utils.dart b/lib/language_utils.dart index 15f18cb..242de69 100644 --- a/lib/language_utils.dart +++ b/lib/language_utils.dart @@ -4,106 +4,21 @@ class LanguageUtils { 'English': 'eng', 'German': 'deu', 'French': 'fra', - 'Spanish': 'spa', - 'Italian': 'ita', - 'Portuguese': 'por', - 'Dutch': 'dut', - 'Polish': 'pol', 'Russian': 'rus', - 'Japanese': 'jpn', - 'Chinese (Simplified)': 'chi_sim', - 'Chinese (Traditional)': 'chi_tra', - 'Korean': 'kor', - 'Arabic': 'ara', - 'Hindi': 'hin', - 'Indonesian': 'ind', - 'Malay': 'mal', - 'Thai': 'tha', - 'Vietnamese': 'vie', - 'Turkish': 'tur', - 'Ukrainian': 'ukr', - 'Hungarian': 'hun', - 'Czech': 'cze', - 'Finnish': 'fin', - 'Danish': 'dan', - 'Norwegian': 'nor', - 'Swedish': 'swe', - 'Greek': 'gre', - 'Slovak': 'slo', - 'Croatian': 'hrv', - 'Lithuanian': 'lit', - 'Romanian': 'rum', - 'Bulgarian': 'bul', - 'Latvian': 'lav', - 'Estonian': 'est', - 'Persian': 'per', - 'Hebrew': 'heb', - 'Serbian': 'srp', - 'Albanian': 'alb', - 'Tagalog': 'tgl', - 'Azerbaijani': 'aze', - 'Basque': 'baq', - 'Belarusian': 'bel', - 'Bengali': 'ben', - 'Bosnian': 'bos', - 'Cebuano': 'ceb', - 'Esperanto': 'epo', - 'Galician': 'glg', - 'Georgian': 'geo', - 'Gujarati': 'guj', }; static Map translatorLanguages = { - 'Auto': 'auto', 'English': 'en', + 'Auto': 'auto', 'German': 'de', 'French': 'fr', - 'Spanish': 'spa', - 'Italian': 'ita', - 'Portuguese': 'por', - 'Dutch': 'dut', - 'Polish': 'pol', - 'Russian': 'rus', - 'Japanese': 'jpn', - 'Chinese (Simplified)': 'chi_sim', - 'Chinese (Traditional)': 'chi_tra', - 'Korean': 'kor', - 'Arabic': 'ara', - 'Hindi': 'hin', - 'Indonesian': 'ind', - 'Malay': 'mal', - 'Thai': 'tha', - 'Vietnamese': 'vie', - 'Turkish': 'tur', - 'Ukrainian': 'ukr', - 'Hungarian': 'hun', - 'Czech': 'cze', - 'Finnish': 'fin', - 'Danish': 'dan', - 'Norwegian': 'nor', - 'Swedish': 'swe', - 'Greek': 'gre', - 'Slovak': 'slo', - 'Croatian': 'hrv', - 'Lithuanian': 'lit', - 'Romanian': 'rum', - 'Bulgarian': 'bul', - 'Latvian': 'lav', - 'Estonian': 'est', - 'Persian': 'per', - 'Hebrew': 'heb', - 'Serbian': 'srp', - 'Albanian': 'alb', - 'Tagalog': 'tgl', - 'Azerbaijani': 'aze', - 'Basque': 'baq', - 'Belarusian': 'bel', - 'Bengali': 'ben', - 'Bosnian': 'bos', - 'Cebuano': 'ceb', - 'Esperanto': 'epo', - 'Galician': 'glg', - 'Georgian': 'geo', - 'Gujarati': 'guj', + 'Russian': 'ru', + }; + + static Map reMapping = { + 'deu': 'de', + 'eng': 'en', + 'fra': 'fr', + 'rus': 'ru', }; } diff --git a/lib/main.dart b/lib/main.dart index 0637718..d4be4e1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,23 @@ +import 'dart:ui'; + import 'package:cpd_app/image_translator.dart'; import 'package:cpd_app/ocr_page.dart'; -import 'package:cpd_app/bottom_bar.dart'; +import 'package:cpd_app/session_list_view.dart'; import 'package:flutter/material.dart'; -void main() { - runApp(const MyApp()); +Future main() async { + FlutterView? flutterView = PlatformDispatcher.instance.views.firstOrNull; + if (flutterView == null || flutterView.physicalSize.isEmpty) { + PlatformDispatcher.instance.onMetricsChanged = () { + flutterView = PlatformDispatcher.instance.views.firstOrNull; + if (flutterView != null && !flutterView!.physicalSize.isEmpty) { + PlatformDispatcher.instance.onMetricsChanged = null; + runApp(const MyApp()); + } + }; + } else { + runApp(const MyApp()); + } } class MyApp extends StatelessWidget { @@ -24,8 +37,9 @@ class OpticText extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color.fromARGB(99, 78, 72, 72), appBar: AppBar( - backgroundColor: Colors.blue, + backgroundColor: const Color.fromARGB(255, 161, 120, 17), title: const Text('Optic Text'), ), body: Center( @@ -36,10 +50,12 @@ class OpticText extends StatelessWidget { MaterialPageRoute(builder: (context) => const ImageToolsPage()), ); }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 161, 120, 17), + foregroundColor: const Color.fromARGB(255, 0, 0, 0)), child: const Text('Image Tools'), ), ), - bottomNavigationBar: const BottomBar(), ); } } @@ -50,7 +66,9 @@ class ImageToolsPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color.fromARGB(99, 78, 72, 72), appBar: AppBar( + backgroundColor: const Color.fromARGB(255, 161, 120, 17), title: const Text('Image Tools'), ), body: Center( @@ -58,12 +76,16 @@ class ImageToolsPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( + key: const Key('ocr_button'), onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => const OCRPage()), ); }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 161, 120, 17), + foregroundColor: const Color.fromARGB(255, 0, 0, 0)), child: const Text('OCR'), ), ElevatedButton( @@ -74,8 +96,24 @@ class ImageToolsPage extends StatelessWidget { builder: (context) => const ImageTranslation()), ); }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 161, 120, 17), + foregroundColor: const Color.fromARGB(255, 0, 0, 0)), child: const Text('Image Translation'), ), + ElevatedButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const SessionListView()), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 161, 120, 17), + foregroundColor: const Color.fromARGB(255, 0, 0, 0)), + child: const Text('Previous Images'), + ), ], ), ), diff --git a/lib/ocr_page.dart b/lib/ocr_page.dart index 267df71..fae8803 100644 --- a/lib/ocr_page.dart +++ b/lib/ocr_page.dart @@ -5,48 +5,98 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'language_utils.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:fluttertoast/fluttertoast.dart'; class OCRPage extends StatefulWidget { const OCRPage({Key? key}) : super(key: key); @override - State createState() => _OCRPageState(); + State createState() => OCRPageState(); } -class _OCRPageState extends State { - String _extractedText = ''; +class OCRPageState extends State { + List tessLangCodes = LanguageUtils.tessLanguages.keys.toList(); + String extractedText = ''; final ImageUploader _imageUploader = ImageUploader(); final HttpUtils _httpUtils = HttpUtils(); String _selectedLanguage = 'Auto'; - XFile? _selectedImage; + XFile? selectedImage; - Future uploadImage( - Uint8List imageBytes, String selectedLanguage) async { + Future getText(Uint8List imageBytes, String selectedLanguage) async { setState(() { - _extractedText = ''; + extractedText = ''; }); - //preprocessing, maybe - String text = ''; - if (selectedLanguage == 'Auto') { - String lang = await _httpUtils.checkLang(imageBytes, 'image.jpg'); - text = await _imageUploader.performOcr(imageBytes, 'image.jpg', lang); - } else { - String langCode = LanguageUtils.tessLanguages[selectedLanguage]!; - text = await _imageUploader.performOcr(imageBytes, 'image.jpg', langCode); + if (!(await _httpUtils.internetConnectivityCheck())) { + _httpUtils.triggerNoInetToast(); + return; + } + String text = ''; + // ignore: use_build_context_synchronously + _httpUtils.showProgressBar(context); + try { + if (Platform.isWindows) { + if (selectedLanguage == 'Auto') { + String lang = await _httpUtils.checkLang(imageBytes, 'image.jpg'); + String mappedLang = LanguageUtils.reMapping[lang]!; + text = + await _httpUtils.extractTextKnownSource(imageBytes, mappedLang); + } else { + String langCode = + LanguageUtils.translatorLanguages[selectedLanguage]!; + text = await _httpUtils.extractTextKnownSource(imageBytes, langCode); + } + } else { + if (selectedLanguage == 'Auto') { + String lang = await _httpUtils.checkLang(imageBytes, 'image.jpg'); + text = await _imageUploader.performOcr(imageBytes, 'image.jpg', lang); + } else { + String langCode = LanguageUtils.tessLanguages[selectedLanguage]!; + text = await _imageUploader.performOcr( + imageBytes, 'image.jpg', langCode); + } + } + } finally { + // ignore: use_build_context_synchronously + _httpUtils.hideProgressBar(context); + } + if (text == "") { + Fluttertoast.showToast( + msg: "Text could not be extracted", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: const Color.fromARGB(255, 161, 120, 17), + textColor: Colors.white, + fontSize: 16.0, + ); + } else { + setState(() { + extractedText = text; + selectedImage = null; + }); } - setState(() { - _extractedText = text; - _selectedImage = null; - }); } - List languages = LanguageUtils.tessLanguages.keys.toList(); @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: const Color.fromARGB(99, 78, 72, 72), appBar: AppBar( + backgroundColor: const Color.fromARGB(255, 161, 120, 17), title: const Text('OCR'), + actions: [ + IconButton( + icon: const Icon(Icons.wifi), + onPressed: () async { + if (!await _httpUtils.internetConnectivityCheck()) { + _httpUtils.triggerNoInetToast(); + } else { + _httpUtils.triggerConnectedToast(); + } + }, + ), + ], ), body: Center( child: Column( @@ -54,23 +104,33 @@ class _OCRPageState extends State { children: [ DropdownButton( value: _selectedLanguage, + style: const TextStyle(color: Colors.white), + dropdownColor: const Color(0xFF4E4848), onChanged: (newValue) { setState(() { _selectedLanguage = newValue!; }); }, - items: languages.map>((String value) { + items: + tessLangCodes.map>((String value) { return DropdownMenuItem( value: value, child: Text(value), ); }).toList(), ), - _selectedImage != null - ? SizedBox( + //Image preview hier + selectedImage != null + ? Container( width: MediaQuery.of(context).size.width * 0.8, height: MediaQuery.of(context).size.height * 0.4, - child: Image.file(File(_selectedImage!.path)), + decoration: BoxDecoration( + border: Border.all( + color: const Color.fromARGB(255, 161, 120, 17), + width: 1.0, + ), + ), + child: Image.file(File(selectedImage!.path)), ) : Container(), ElevatedButton( @@ -80,33 +140,44 @@ class _OCRPageState extends State { await picker.pickImage(source: ImageSource.gallery); if (image != null) { setState(() { - _selectedImage = image; + selectedImage = image; }); } }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 161, 120, 17), + foregroundColor: const Color.fromARGB(255, 0, 0, 0)), child: const Text('Select Image'), ), ElevatedButton( - onPressed: _selectedImage != null + onPressed: selectedImage != null ? () async { - if (_selectedImage != null) { - File imageFile = File(_selectedImage!.path); + if (selectedImage != null && selectedImage!.path != "") { + File imageFile = File(selectedImage!.path); List imageBytes = imageFile.readAsBytesSync(); - await uploadImage( + await getText( Uint8List.fromList(imageBytes), _selectedLanguage); + } else if (selectedImage != null && + selectedImage!.path == "") { + Uint8List? list = await selectedImage?.readAsBytes(); + await getText(list!, _selectedLanguage); } } : null, + style: ElevatedButton.styleFrom( + backgroundColor: const Color.fromARGB(255, 161, 120, 17), + foregroundColor: const Color.fromARGB(255, 0, 0, 0)), child: const Text('Extract Text'), ), - _extractedText.isNotEmpty + extractedText.isNotEmpty ? Expanded( child: Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( readOnly: true, - initialValue: _extractedText, + initialValue: extractedText, maxLines: null, + style: const TextStyle(color: Colors.white), decoration: const InputDecoration( labelText: 'Extracted Text', border: OutlineInputBorder(), @@ -115,12 +186,19 @@ class _OCRPageState extends State { ), ) : Container(), - TextButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: _extractedText)); - }, - child: const Text('Copy to Clipboard'), - ), + + extractedText.isNotEmpty + ? TextButton( + onPressed: () { + Clipboard.setData(ClipboardData(text: extractedText)); + }, + style: TextButton.styleFrom( + backgroundColor: const Color.fromARGB(0, 33, 149, 243), + foregroundColor: const Color.fromARGB(255, 161, 120, 17), + ), + child: const Text('Copy to Clipboard'), + ) + : const SizedBox(), ], ), ), diff --git a/lib/session_list.dart b/lib/session_list.dart new file mode 100644 index 0000000..8960acd --- /dev/null +++ b/lib/session_list.dart @@ -0,0 +1,13 @@ +import 'dart:typed_data'; + +class ImageList { + static Map images = {}; + + static Map getImages() { + return images; + } + + static void addImage(String name, Uint8List image) { + images[name] = image; + } +} diff --git a/lib/session_list_view.dart b/lib/session_list_view.dart new file mode 100644 index 0000000..204c611 --- /dev/null +++ b/lib/session_list_view.dart @@ -0,0 +1,77 @@ +import 'dart:typed_data'; +import 'package:cpd_app/file_utils.dart'; +import 'package:cpd_app/session_list.dart'; +import 'package:flutter/material.dart'; + +class SessionListView extends StatefulWidget { + const SessionListView({super.key}); + + @override + State createState() => _SessionListViewState(); +} + +class _SessionListViewState extends State { + final Map _images = ImageList.getImages(); + String? _selectedImageKey; + bool _showSaveButton = false; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color.fromARGB(99, 78, 72, 72), + appBar: AppBar( + backgroundColor: const Color.fromARGB(255, 161, 120, 17), + title: const Text('Image Gallery'), + ), + body: GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 4.0, + mainAxisSpacing: 4.0, + ), + itemCount: _images.length, + itemBuilder: (BuildContext context, int index) { + String imageKey = _images.keys.elementAt(index); + Uint8List imageData = _images[imageKey]!; + return GestureDetector( + onTap: () { + setState(() { + _selectedImageKey = imageKey; + _showSaveButton = true; + }); + }, + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: _selectedImageKey == imageKey + ? Colors.blue + : Colors.transparent, + width: 2.0, + ), + ), + child: Image.memory( + imageData, + fit: BoxFit.cover, + ), + ), + ); + }, + ), + floatingActionButton: _showSaveButton + ? FloatingActionButton( + onPressed: () { + saveImage(_selectedImageKey!); + setState(() { + _showSaveButton = false; + }); + }, + child: const Icon(Icons.save), + ) + : null, + ); + } + + void saveImage(String imageKey) { + FileUtils.saveTranslatedImage(ImageList.getImages()[imageKey]!); + } +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b878e03..e62651d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import connectivity_macos import file_selector_macos import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 4ed0bb9..fc2f1a0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + connectivity: + dependency: "direct main" + description: + name: connectivity + sha256: a8e91263cf3e25fb5cc95e19dfde4999e32a648ac3b9e8a558a28165731678f8 + url: "https://pub.dev" + source: hosted + version: "3.0.6" + connectivity_for_web: + dependency: transitive + description: + name: connectivity_for_web + sha256: "01a390c1d5adc2ed1fa1f52d120c07fe9fd01166a93f965a832fd6cfc0ea6482" + url: "https://pub.dev" + source: hosted + version: "0.4.0+1" + connectivity_macos: + dependency: transitive + description: + name: connectivity_macos + sha256: "51ae08d5162eca9669b9d8951ed83ce19c5355a81149f94e4dee2740beb93628" + url: "https://pub.dev" + source: hosted + version: "0.2.1+2" + connectivity_platform_interface: + dependency: transitive + description: + name: connectivity_platform_interface + sha256: "2d82e942df9d49f29a24bb07fb5ce085d4a53e47818c62364d2b6deb9e0d7a8e" + url: "https://pub.dev" + source: hosted + version: "2.0.1" convert: dependency: transitive description: @@ -256,6 +288,14 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 + url: "https://pub.dev" + source: hosted + version: "8.2.4" glob: dependency: transitive description: @@ -280,6 +320,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_gallery_saver: + dependency: "direct main" + description: + name: image_gallery_saver + sha256: "0aba74216a4d9b0561510cb968015d56b701ba1bd94aace26aacdd8ae5761816" + url: "https://pub.dev" + source: hosted + version: "2.0.3" image_picker: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index a236ea2..0c4e1ed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: flutter: sdk: flutter - dio: ^4.0.0 # For making HTTP requests + dio: ^4.0.0 file_picker: ^4.1.4 image_picker: ^0.8.1 cupertino_icons: ^1.0.2 @@ -17,8 +17,11 @@ dependencies: http: ^1.1.0 flutter_tesseract_ocr: http_parser: ^4.0.2 - #opencv_4: ^1.0.0 + fluttertoast: ^8.0.8 + connectivity: ^3.0.6 + image_gallery_saver: '^2.0.3' + dev_dependencies: flutter_test: sdk: flutter @@ -27,14 +30,12 @@ dev_dependencies: flutter: assets: - - assets/ + - assets/schild.png - assets/tessdata_config.json - assets/tessdata/eng.traineddata - assets/tessdata/deu.traineddata - assets/tessdata/jpn.traineddata - - assets/tessdata/chi_sim.traineddata - - assets/tessdata/ell.traineddata - - assets/tessdata/ara.traineddata + - assets/tessdata/fra.traineddata - assets/tessdata/rus.traineddata uses-material-design: true \ No newline at end of file diff --git a/tessdata/rus.traineddata b/tessdata/rus.traineddata deleted file mode 100644 index b146cb2..0000000 Binary files a/tessdata/rus.traineddata and /dev/null differ diff --git a/test/widget_test.dart b/test/widget_test.dart index ffd0467..9fe8502 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,21 +1,125 @@ -import 'package:cpd_app/image_uploader.dart'; +import 'dart:io'; +import 'package:cpd_app/session_list.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:cpd_app/main.dart'; +import 'package:cpd_app/ocr_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; + +Future loadImageBytesFromAssets(String imagePath) async { + final ByteData data = await rootBundle.load(imagePath); + return data.buffer.asUint8List(); +} void main() { - TestWidgetsFlutterBinding.ensureInitialized(); + setUpAll(() { + HttpOverrides.global = null; + }); + testWidgets('Check ocr UI', (WidgetTester tester) async { + await tester.pumpWidget(const MyApp()); - test('Test OCR functionality', () async { - final ImageUploader imageUploader = ImageUploader(); - String imageName = "lorem.png"; + final buttonsFinder = find.byType(ElevatedButton); - var img = await imageUploader.buildImageFile(imageName); - assert(img.lengthInBytes > 0); + expect(buttonsFinder, findsWidgets); + await tester.tap(find.widgetWithText(ElevatedButton, "Image Tools")); + await tester.pumpAndSettle(); + expect(find.text('OCR'), findsOneWidget); - // momentan auskommentiert, weil das pathproviderplugin irgentwie gemockt werden muss und ich es noch nicht hinbekommen habe - //mock response, weil man keine http requests in tests machen kann - // String mockResponse = "eng"; + var ocrbtn = find.widgetWithText(ElevatedButton, "OCR"); + expect(ocrbtn, findsOneWidget); - // String text = await imageUploader.performOcr(img, imageName, mockResponse); - // assert(text.contains("Lorem ipsum")); + var imgtrans = find.widgetWithText(ElevatedButton, "Image Translation"); + expect(imgtrans, findsOneWidget); + var prev = find.widgetWithText(ElevatedButton, "Previous Images"); + expect(prev, findsOneWidget); + + await tester.tap(find.widgetWithText(ElevatedButton, "OCR")); + await tester.pumpAndSettle(); + + expect(find.text('OCR'), findsOneWidget); + expect(find.text('Select Image'), findsOneWidget); + expect(find.widgetWithText(DropdownButton, 'Auto'), findsOneWidget); + await tester.tap(find.widgetWithText(DropdownButton, 'Auto')); + await tester.pumpAndSettle(); + expect(find.text('German'), findsOneWidget); + expect(find.text('English'), findsOneWidget); + expect(find.text('French'), findsOneWidget); + expect(find.text('Russian'), findsOneWidget); + await tester.tap(find.widgetWithText(DropdownMenuItem, 'German')); + await tester.tap(find.widgetWithText(ElevatedButton, "Select Image")); + await tester.pump(); + + final ocrFinder = find.byType(OCRPage); + final ocrState = tester.state(ocrFinder); + Uint8List bytes = await loadImageBytesFromAssets('assets/schild.png'); + + ocrState.setState(() { + var image = XFile.fromData(bytes); + ocrState.selectedImage = image; + }); + expect(ocrState.selectedImage, isNotNull); + await tester.pumpAndSettle(); + + expect(find.widgetWithText(ElevatedButton, "Extract Text"), findsOneWidget); + }); + testWidgets('Check imageTranslateUI', (WidgetTester tester) async { + await tester.pumpWidget(const MyApp()); + final buttonsFinder = find.byType(ElevatedButton); + expect(buttonsFinder, findsWidgets); + await tester.tap(find.widgetWithText(ElevatedButton, "Image Tools")); + await tester.pumpAndSettle(); + expect(find.text('Image Translation'), findsOneWidget); + var btn1 = find.widgetWithText(ElevatedButton, "Image Translation"); + expect(btn1, findsOneWidget); + await tester.tap(btn1); + await tester.pumpAndSettle(); + expect(find.text('Image Translation'), findsOneWidget); + expect( + find.widgetWithText(DropdownButton, 'English'), findsOneWidget); + await tester.tap(find.widgetWithText(DropdownButton, 'English')); + await tester.pumpAndSettle(); + expect(find.text('German'), findsExactly(2)); + expect(find.text('English'), findsExactly(2)); + expect(find.text('French'), findsExactly(1)); + expect(find.text('Russian'), findsExactly(1)); + await tester.pumpAndSettle(); + await tester.tap(find.widgetWithText(DropdownButton, 'German')); + expect(find.text('German'), findsExactly(2)); + expect(find.text('English'), findsExactly(2)); + expect(find.text('French'), findsExactly(1)); + expect(find.text('Russian'), findsExactly(1)); + // + }); + + testWidgets('Check listPreview', (WidgetTester tester) async { + ImageList.addImage( + "test1", await loadImageBytesFromAssets('assets/schild.png')); + ImageList.addImage( + "test2", await loadImageBytesFromAssets('assets/schild.png')); + ImageList.addImage( + "test3", await loadImageBytesFromAssets('assets/schild.png')); + ImageList.addImage( + "test3", await loadImageBytesFromAssets('assets/schild.png')); + + await tester.pumpWidget(const MyApp()); + + final buttonsFinder = find.byType(ElevatedButton); + expect(buttonsFinder, findsWidgets); + + await tester.tap(find.widgetWithText(ElevatedButton, "Image Tools")); + await tester.pumpAndSettle(); + + var btn1 = find.widgetWithText(ElevatedButton, "Previous Images"); + expect(btn1, findsOneWidget); + await tester.tap(btn1); + await tester.pumpAndSettle(); + expect(find.text('Image Gallery'), findsOneWidget); + + final gridViewFinder = find.byType(GridView); + expect(gridViewFinder, findsOneWidget); + + const numberOfEntries = 4; + expect(find.byType(GestureDetector), findsNWidgets(numberOfEntries)); }); } diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index 930d207..903f489 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS