Compare commits
No commits in common. "d5e3d8b76c35c9f45b087e14cd6302459a3399cc" and "ba04bd5a1cb243ec011d9465562f93f227131b0e" have entirely different histories.
d5e3d8b76c
...
ba04bd5a1c
13 changed files with 68 additions and 142 deletions
2
.flutter
2
.flutter
|
@ -1 +1 @@
|
||||||
Subproject commit 0d074ced6cd64f39f586aaa115b2e4c993d74cb7
|
Subproject commit 123453dc41ef48e717939dd3c012db6a23138895
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"conventionalCommits.scopes": [
|
"conventionalCommits.scopes": [
|
||||||
"ocr",
|
"ocr"
|
||||||
"ui"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -20,4 +20,4 @@ Expense manager
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
```
|
```
|
||||||
|
|
||||||
The base app includes the [tessdata_fast](https://github.com/tesseract-ocr/tessdata_fast) English trained data, ©️ [tessdata_fast / Tesseract contributors](https://github.com/tesseract-ocr/tessdata_fast/graphs/contributors), used under the [Apache 2.0 license](https://github.com/tesseract-ocr/tessdata_fast/blob/main/LICENSE)
|
The base app includes the [tessdata_best](https://github.com/tesseract-ocr/tessdata_best) English trained data, ©️ [tessdata_best / Tesseract contributors](https://github.com/tesseract-ocr/tessdata_best/graphs/contributors), used under the [Apache 2.0 license](https://github.com/tesseract-ocr/tessdata_best/blob/main/LICENSE)
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="Prašule"
|
android:label="Prašule"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|
Binary file not shown.
2
flutterw
2
flutterw
|
@ -4,7 +4,7 @@
|
||||||
##
|
##
|
||||||
## Flutter start up script for UN*X
|
## Flutter start up script for UN*X
|
||||||
## Version: v1.3.1
|
## Version: v1.3.1
|
||||||
## Date: 2023-09-28 16:13:48
|
## Date: 2023-09-05 17:10:03
|
||||||
##
|
##
|
||||||
## Use this flutter wrapper to bundle Flutter within your project to make
|
## Use this flutter wrapper to bundle Flutter within your project to make
|
||||||
## sure everybody builds with the same version.
|
## sure everybody builds with the same version.
|
||||||
|
|
|
@ -12,7 +12,7 @@ class TessdataApi {
|
||||||
);
|
);
|
||||||
static Future<List<String>> getAvailableData() async {
|
static Future<List<String>> getAvailableData() async {
|
||||||
var res = await _client.get(
|
var res = await _client.get(
|
||||||
"https://git.mnau.xyz/api/v1/repos/hernik/tessdata_fast/contents",
|
"https://git.mnau.xyz/api/v1/repos/hernik/tessdata_best/contents",
|
||||||
options: Options(headers: {"Accept": "application/json"}));
|
options: Options(headers: {"Accept": "application/json"}));
|
||||||
if ((res.statusCode ?? 500) > 399) {
|
if ((res.statusCode ?? 500) > 399) {
|
||||||
return Future.error("The server returned status code ${res.statusCode}");
|
return Future.error("The server returned status code ${res.statusCode}");
|
||||||
|
@ -28,36 +28,25 @@ class TessdataApi {
|
||||||
|
|
||||||
static Future<void> deleteData(String name) async {
|
static Future<void> deleteData(String name) async {
|
||||||
var dataDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
var dataDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
||||||
if (!dataDir.existsSync()) {
|
|
||||||
dataDir.createSync();
|
|
||||||
}
|
|
||||||
var dataFile = File("${dataDir.path}/$name.traineddata");
|
var dataFile = File("${dataDir.path}/$name.traineddata");
|
||||||
if (!dataFile.existsSync()) return;
|
if (!dataFile.existsSync()) return;
|
||||||
dataFile.deleteSync();
|
dataFile.deleteSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<String>> getDownloadedData() async {
|
static Future<List<String>> getDownloadedData() async =>
|
||||||
var tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
Directory(await FlutterTesseractOcr.getTessdataPath())
|
||||||
if (!tessDir.existsSync()) {
|
|
||||||
tessDir.createSync();
|
|
||||||
}
|
|
||||||
return tessDir
|
|
||||||
.listSync()
|
.listSync()
|
||||||
.where((element) => element.path.endsWith(".traineddata"))
|
.where((element) => element.path.endsWith(".traineddata"))
|
||||||
.map<String>((e) => e.path.split("/").last)
|
.map<String>((e) => e.path.split("/").last)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
|
||||||
|
|
||||||
static Future<void> downloadData(String isoCode,
|
static Future<void> downloadData(String isoCode,
|
||||||
{void Function(int, int)? callback}) async {
|
{void Function(int, int)? callback}) async {
|
||||||
var tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
var file = File(
|
||||||
if (!tessDir.existsSync()) {
|
"${(await FlutterTesseractOcr.getTessdataPath())}/$isoCode.traineddata");
|
||||||
tessDir.createSync();
|
|
||||||
}
|
|
||||||
var file = File("${tessDir.path}/$isoCode.traineddata");
|
|
||||||
if (file.existsSync()) return; // TODO: maybe ask to redownload?
|
if (file.existsSync()) return; // TODO: maybe ask to redownload?
|
||||||
var res = await _client.get(
|
var res = await _client.get(
|
||||||
"https://git.mnau.xyz/hernik/tessdata_fast/raw/branch/main/$isoCode.traineddata",
|
"https://git.mnau.xyz/hernik/tessdata_best/raw/branch/main/$isoCode.traineddata",
|
||||||
options: Options(responseType: ResponseType.bytes),
|
options: Options(responseType: ResponseType.bytes),
|
||||||
onReceiveProgress: callback);
|
onReceiveProgress: callback);
|
||||||
if ((res.statusCode ?? 500) > 399) {
|
if ((res.statusCode ?? 500) > 399) {
|
||||||
|
|
|
@ -35,9 +35,7 @@ class PlatformField extends PlatformWidget<TextField, CupertinoTextField> {
|
||||||
controller: controller,
|
controller: controller,
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(labelText: labelText),
|
||||||
labelText: labelText,
|
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(4))),
|
|
||||||
autocorrect: autocorrect,
|
autocorrect: autocorrect,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
style: textStyle,
|
style: textStyle,
|
||||||
|
|
|
@ -53,10 +53,10 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
const Text("Name"),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.8,
|
width: MediaQuery.of(context).size.width * 0.8,
|
||||||
child: PlatformField(
|
child: PlatformField(
|
||||||
labelText: "Name",
|
|
||||||
controller: TextEditingController(text: newEntry.name),
|
controller: TextEditingController(text: newEntry.name),
|
||||||
onChanged: (v) {
|
onChanged: (v) {
|
||||||
newEntry.name = v;
|
newEntry.name = v;
|
||||||
|
@ -66,10 +66,10 @@ class _CreateEntryViewState extends State<CreateEntryView> {
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
height: 15,
|
height: 15,
|
||||||
),
|
),
|
||||||
|
const Text("Amount"),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: MediaQuery.of(context).size.width * 0.8,
|
width: MediaQuery.of(context).size.width * 0.8,
|
||||||
child: PlatformField(
|
child: PlatformField(
|
||||||
labelText: "Amount",
|
|
||||||
controller:
|
controller:
|
||||||
TextEditingController(text: newEntry.amount.toString()),
|
TextEditingController(text: newEntry.amount.toString()),
|
||||||
keyboardType:
|
keyboardType:
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||||
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
|
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
|
||||||
import 'package:flutter_tesseract_ocr/flutter_tesseract_ocr.dart';
|
|
||||||
import 'package:grouped_list/grouped_list.dart';
|
import 'package:grouped_list/grouped_list.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:intl/date_symbol_data_local.dart';
|
import 'package:intl/date_symbol_data_local.dart';
|
||||||
|
@ -15,7 +14,6 @@ import 'package:prasule/pw/platformbutton.dart';
|
||||||
import 'package:prasule/pw/platformdialog.dart';
|
import 'package:prasule/pw/platformdialog.dart';
|
||||||
import 'package:prasule/views/create_entry.dart';
|
import 'package:prasule/views/create_entry.dart';
|
||||||
import 'package:prasule/views/settings/settings.dart';
|
import 'package:prasule/views/settings/settings.dart';
|
||||||
import 'package:prasule/views/settings/tessdata_list.dart';
|
|
||||||
import 'package:prasule/views/setup.dart';
|
import 'package:prasule/views/setup.dart';
|
||||||
|
|
||||||
class HomeView extends StatefulWidget {
|
class HomeView extends StatefulWidget {
|
||||||
|
@ -87,8 +85,48 @@ class _HomeViewState extends State<HomeView> {
|
||||||
SpeedDialChild(
|
SpeedDialChild(
|
||||||
child: const Icon(Icons.image),
|
child: const Icon(Icons.image),
|
||||||
label: "Add through saved image",
|
label: "Add through saved image",
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
startOcr(ImageSource.gallery);
|
var availableLanguages = await TessdataApi.getDownloadedData();
|
||||||
|
if (mounted) {
|
||||||
|
var selectedLanguages =
|
||||||
|
List<bool>.filled(availableLanguages.length, false);
|
||||||
|
selectedLanguages[
|
||||||
|
availableLanguages.indexOf("eng.traineddata")] = true;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (c) => PlatformDialog(
|
||||||
|
title: "Select languages for OCR",
|
||||||
|
content: Column(
|
||||||
|
children: [
|
||||||
|
...List.generate(
|
||||||
|
availableLanguages.length,
|
||||||
|
(index) => Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: selectedLanguages[index],
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null ||
|
||||||
|
(selectedLanguages
|
||||||
|
.where((element) => element)
|
||||||
|
.length <=
|
||||||
|
1 &&
|
||||||
|
!value)) return;
|
||||||
|
selectedLanguages[index] = value;
|
||||||
|
setState(() {}); // todo: builder
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
|
Text(availableLanguages[index].split(".").first)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -248,102 +286,6 @@ class _HomeViewState extends State<HomeView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> startOcr(ImageSource imgSrc) async {
|
|
||||||
var availableLanguages = await TessdataApi.getDownloadedData();
|
|
||||||
if (availableLanguages.isEmpty) {
|
|
||||||
if (!mounted) return;
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content:
|
|
||||||
const Text("You do not have any OCR language data downloaded"),
|
|
||||||
action: SnackBarAction(
|
|
||||||
label: "Download",
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push(
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (c) => const TessdataListView(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!mounted) return;
|
|
||||||
var selectedLanguages = List<bool>.filled(availableLanguages.length, false);
|
|
||||||
if (selectedLanguages.length == 1) {
|
|
||||||
selectedLanguages[0] = true;
|
|
||||||
}
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (c) => PlatformDialog(
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final ImagePicker picker = ImagePicker();
|
|
||||||
final XFile? media = await picker.pickImage(source: imgSrc);
|
|
||||||
if (media == null) {
|
|
||||||
if (mounted) Navigator.of(context).pop();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// get selected languages
|
|
||||||
var selected = availableLanguages
|
|
||||||
.where((element) =>
|
|
||||||
selectedLanguages[availableLanguages.indexOf(element)])
|
|
||||||
.join("+")
|
|
||||||
.replaceAll(".traineddata", "");
|
|
||||||
logger.i(selected);
|
|
||||||
var string = await FlutterTesseractOcr.extractText(media.path,
|
|
||||||
language: selected,
|
|
||||||
args: {
|
|
||||||
//"psm": "4",
|
|
||||||
"preserve_interword_spaces": "1",
|
|
||||||
});
|
|
||||||
logger.i(string);
|
|
||||||
if (mounted) Navigator.of(context).pop();
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
child: const Text("Ok")),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(c).pop();
|
|
||||||
},
|
|
||||||
child: const Text("Cancel")),
|
|
||||||
],
|
|
||||||
title: "Select languages for OCR",
|
|
||||||
content: Column(
|
|
||||||
children: [
|
|
||||||
...List.generate(
|
|
||||||
availableLanguages.length,
|
|
||||||
(index) => Row(
|
|
||||||
children: [
|
|
||||||
Checkbox(
|
|
||||||
value: selectedLanguages[index],
|
|
||||||
onChanged: (value) {
|
|
||||||
if (value == null ||
|
|
||||||
(selectedLanguages
|
|
||||||
.where((element) => element)
|
|
||||||
.length <=
|
|
||||||
1 &&
|
|
||||||
!value)) return;
|
|
||||||
selectedLanguages[index] = value;
|
|
||||||
setState(() {}); // todo: builder
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
width: 10,
|
|
||||||
),
|
|
||||||
Text(availableLanguages[index].split(".").first)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> getLostData() async {
|
Future<void> getLostData() async {
|
||||||
final ImagePicker picker = ImagePicker();
|
final ImagePicker picker = ImagePicker();
|
||||||
final LostDataResponse response = await picker.retrieveLostData();
|
final LostDataResponse response = await picker.retrieveLostData();
|
||||||
|
|
|
@ -133,7 +133,6 @@ class _TessdataListViewState extends State<TessdataListView> {
|
||||||
/// Used to find which `.traineddata` is already downloaded and which not
|
/// Used to find which `.traineddata` is already downloaded and which not
|
||||||
/// so we can show it to the user
|
/// so we can show it to the user
|
||||||
void loadAllTessdata() async {
|
void loadAllTessdata() async {
|
||||||
var tessDir = Directory(await FlutterTesseractOcr.getTessdataPath());
|
|
||||||
var d = await TessdataApi.getAvailableData();
|
var d = await TessdataApi.getAvailableData();
|
||||||
var dataStatus = <Map<String, bool>>[];
|
var dataStatus = <Map<String, bool>>[];
|
||||||
for (var data in d) {
|
for (var data in d) {
|
||||||
|
@ -141,7 +140,8 @@ class _TessdataListViewState extends State<TessdataListView> {
|
||||||
e[data] = false;
|
e[data] = false;
|
||||||
dataStatus.add(e);
|
dataStatus.add(e);
|
||||||
}
|
}
|
||||||
var appDir = tessDir.listSync();
|
var appDir =
|
||||||
|
Directory(await FlutterTesseractOcr.getTessdataPath()).listSync();
|
||||||
for (var file in appDir) {
|
for (var file in appDir) {
|
||||||
if (file is! File ||
|
if (file is! File ||
|
||||||
!file.path.endsWith("traineddata") ||
|
!file.path.endsWith("traineddata") ||
|
||||||
|
|
|
@ -779,10 +779,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: platform
|
name: platform
|
||||||
sha256: "57c07bf82207aee366dfaa3867b3164e4f03a238a461a11b0e8a3a510d51203d"
|
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.0"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -1024,10 +1024,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
|
sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "11.10.0"
|
version: "11.9.0"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
Loading…
Reference in a new issue