diff --git a/.metadata b/.metadata index ab3e1c0..3e6e02a 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "ead455963c12b453cdb2358cad34969c76daf180" + revision: "78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9" channel: "stable" project_type: app @@ -13,26 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: ead455963c12b453cdb2358cad34969c76daf180 - base_revision: ead455963c12b453cdb2358cad34969c76daf180 + create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - platform: android - create_revision: ead455963c12b453cdb2358cad34969c76daf180 - base_revision: ead455963c12b453cdb2358cad34969c76daf180 + create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - platform: ios - create_revision: ead455963c12b453cdb2358cad34969c76daf180 - base_revision: ead455963c12b453cdb2358cad34969c76daf180 + create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - platform: linux - create_revision: ead455963c12b453cdb2358cad34969c76daf180 - base_revision: ead455963c12b453cdb2358cad34969c76daf180 + create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - platform: macos - create_revision: ead455963c12b453cdb2358cad34969c76daf180 - base_revision: ead455963c12b453cdb2358cad34969c76daf180 + create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - platform: web - create_revision: ead455963c12b453cdb2358cad34969c76daf180 - base_revision: ead455963c12b453cdb2358cad34969c76daf180 + create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 - platform: windows - create_revision: ead455963c12b453cdb2358cad34969c76daf180 - base_revision: ead455963c12b453cdb2358cad34969c76daf180 + create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 # User provided section diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e46adca --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,42 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + + + { + "name": "optictext", + "request": "launch", + "type": "dart" + }, + { + "name": "optictext (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "optictext (release mode)", + "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/lib/http_utils.dart b/lib/http_utils.dart index 0fe1eab..78ff06a 100644 --- a/lib/http_utils.dart +++ b/lib/http_utils.dart @@ -5,8 +5,9 @@ import 'package:http_parser/http_parser.dart'; class HttpUtils { var client = http.Client(); - Future performHttpRequest( - Uint8List imageBytes, String imageName) async { + Future checkLang(Uint8List imageBytes, String imageName) async { + var client = http.Client(); + var postUri = Uri.parse("http://130.61.88.150/upload"); http.MultipartRequest request = http.MultipartRequest("POST", postUri); diff --git a/lib/image_translator.dart b/lib/image_translator.dart new file mode 100644 index 0000000..d7a3c05 --- /dev/null +++ b/lib/image_translator.dart @@ -0,0 +1,218 @@ +import 'dart:io'; +import 'package:cpd_app/http_utils.dart'; +import 'package:http/http.dart' as http; +import 'package:cpd_app/image_uploader.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'language_utils.dart'; +import 'package:image_picker/image_picker.dart'; + +class ImageTranslation extends StatefulWidget { + const ImageTranslation({Key? key}) : super(key: key); + + @override + State createState() => _ImageTranslationState(); +} + +class _ImageTranslationState extends State { + XFile? _selectedImage; + String _sourceLanguage = 'Auto'; + String _targetLanguage = 'English'; + String _translatedText = ''; + final ImageUploader _imageUploader = ImageUploader(); + 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 + */ + 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); + } + 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 (response.statusCode == 200) { + _translatedText = response.body; + setState(() { + _translatedText = text; + _selectedImage = null; + }); + } else { + print('Error: ${response.statusCode}'); + } + print(_translatedText); + } + } + + List sourceLanguages = LanguageUtils.tessLanguages.keys.toList(); + List targetLanguages = + LanguageUtils.translatorLanguages.keys.toList(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Image Translation'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // From - Subtle blue background + Container( + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(8.0), + ), + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('From', style: TextStyle(color: Colors.blue)), + DropdownButton( + value: _sourceLanguage, + onChanged: (newValue) { + setState(() { + _sourceLanguage = newValue!; + }); + }, + items: sourceLanguages + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ], + ), + ), + // Target - Subtle red background + Container( + decoration: BoxDecoration( + color: Colors.red.shade100, + borderRadius: BorderRadius.circular(8.0), + ), + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Target', style: TextStyle(color: Colors.red)), + DropdownButton( + value: _targetLanguage, + onChanged: (newValue) { + setState(() { + _targetLanguage = newValue!; + }); + }, + items: targetLanguages + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), + ), + ], + ), + ), + ], + ), + 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; + }); + } + }, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Colors.blue), + ), + child: const Text( + 'Upload Image', + style: TextStyle(color: Colors.white), + ), + ), + ElevatedButton( + onPressed: _selectedImage != null + ? () async { + if (_selectedImage != null) { + File imageFile = File(_selectedImage!.path); + List imageBytes = imageFile.readAsBytesSync(); + await uploadImage(Uint8List.fromList(imageBytes), + _sourceLanguage, _targetLanguage); + } + } + : 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'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/image_uploader.dart b/lib/image_uploader.dart index 7b8e212..6722d6e 100644 --- a/lib/image_uploader.dart +++ b/lib/image_uploader.dart @@ -4,8 +4,6 @@ import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart'; import 'package:path_provider/path_provider.dart'; class ImageUploader { - //final http.Client client; - ImageUploader(); Future buildImageFile(String img) async { @@ -30,4 +28,21 @@ class ImageUploader { return text; } + + Future performAdvancedOCR( + Uint8List imageBytes, String imageName, String lang) async { + Directory tempDir = await getTemporaryDirectory(); + String tempPath = tempDir.path; + File tempImageFile = File('$tempPath/$imageName'); + await tempImageFile.writeAsBytes(imageBytes); + + String text = await FlutterTesseractOcr.extractHocr(tempImageFile.path, + language: lang, + args: { + "psm": "4", + "preserve_interword_spaces": "1", + }); + + return text; + } } diff --git a/lib/language_utils.dart b/lib/language_utils.dart new file mode 100644 index 0000000..15f18cb --- /dev/null +++ b/lib/language_utils.dart @@ -0,0 +1,109 @@ +class LanguageUtils { + static Map tessLanguages = { + 'Auto': 'auto', + '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', + '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', + }; +} diff --git a/lib/main.dart b/lib/main.dart index e587008..0637718 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:cpd_app/image_translator.dart'; import 'package:cpd_app/ocr_page.dart'; import 'package:cpd_app/bottom_bar.dart'; import 'package:flutter/material.dart'; @@ -66,7 +67,13 @@ class ImageToolsPage extends StatelessWidget { child: const Text('OCR'), ), ElevatedButton( - onPressed: () {}, + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ImageTranslation()), + ); + }, child: const Text('Image Translation'), ), ], diff --git a/lib/ocr_page.dart b/lib/ocr_page.dart index ac526b8..267df71 100644 --- a/lib/ocr_page.dart +++ b/lib/ocr_page.dart @@ -1,10 +1,13 @@ +import 'dart:io'; import 'package:cpd_app/http_utils.dart'; import 'package:cpd_app/image_uploader.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'language_utils.dart'; +import 'package:image_picker/image_picker.dart'; class OCRPage extends StatefulWidget { - const OCRPage({super.key}); + const OCRPage({Key? key}) : super(key: key); @override State createState() => _OCRPageState(); @@ -14,25 +17,31 @@ class _OCRPageState extends State { String _extractedText = ''; final ImageUploader _imageUploader = ImageUploader(); final HttpUtils _httpUtils = HttpUtils(); - @override - void initState() { - super.initState(); - uploadImage("lorem.png"); - } + String _selectedLanguage = 'Auto'; + XFile? _selectedImage; - Future uploadImage(String img) async { - //hier wird der user sein bild auswählen können - //aktuell hardcoded bild zum testen - String imageName = img; - Uint8List imageBytes = await _imageUploader.buildImageFile(imageName); - String lang = await _httpUtils.performHttpRequest(imageBytes, imageName); - String text = await _imageUploader.performOcr(imageBytes, imageName, lang); + Future uploadImage( + Uint8List imageBytes, String selectedLanguage) async { + setState(() { + _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); + } setState(() { _extractedText = text; + _selectedImage = null; }); } + List languages = LanguageUtils.tessLanguages.keys.toList(); @override Widget build(BuildContext context) { return Scaffold( @@ -43,11 +52,69 @@ class _OCRPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - ElevatedButton( - onPressed: () => uploadImage("lorem.png"), - child: const Text('Test Upload Image'), + DropdownButton( + value: _selectedLanguage, + onChanged: (newValue) { + setState(() { + _selectedLanguage = newValue!; + }); + }, + items: languages.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value), + ); + }).toList(), ), - Text(_extractedText), + _selectedImage != null + ? SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.height * 0.4, + child: Image.file(File(_selectedImage!.path)), + ) + : Container(), + ElevatedButton( + onPressed: () async { + final ImagePicker picker = ImagePicker(); + final XFile? image = + await picker.pickImage(source: ImageSource.gallery); + if (image != null) { + setState(() { + _selectedImage = image; + }); + } + }, + child: const Text('Select Image'), + ), + ElevatedButton( + onPressed: _selectedImage != null + ? () async { + if (_selectedImage != null) { + File imageFile = File(_selectedImage!.path); + List imageBytes = imageFile.readAsBytesSync(); + await uploadImage( + Uint8List.fromList(imageBytes), _selectedLanguage); + } + } + : null, + child: const Text('Extract Text'), + ), + _extractedText.isNotEmpty + ? Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + readOnly: true, + initialValue: _extractedText, + maxLines: null, + decoration: const InputDecoration( + labelText: 'Extracted Text', + border: OutlineInputBorder(), + ), + ), + ), + ) + : Container(), TextButton( onPressed: () { Clipboard.setData(ClipboardData(text: _extractedText)); diff --git a/pubspec.lock b/pubspec.lock index 52a6447..4ed0bb9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" convert: dependency: transitive description: @@ -388,10 +388,10 @@ packages: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" mime: dependency: transitive description: @@ -521,18 +521,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" string_scanner: dependency: transitive description: @@ -553,10 +553,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" typed_data: dependency: transitive description: @@ -585,10 +585,10 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.3.0" win32: dependency: transitive description: @@ -614,5 +614,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.3 <4.0.0" + dart: ">=3.2.0-194.0.dev <4.0.0" flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index 99a9f5c..a236ea2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: http: ^1.1.0 flutter_tesseract_ocr: http_parser: ^4.0.2 + #opencv_4: ^1.0.0 dev_dependencies: flutter_test: