From 190831876906ff60594814a27ee0e41b7f9e61cf Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Thu, 22 May 2025 16:51:13 +0700 Subject: [PATCH] Refactor DocumentIntelligenceService and related classes to comment out code for future reference; update AuthenticationRepository to remove splash screen on app start. --- sigap-mobile/lib/main.dart | 5 +- .../src/cores/services/azure_ocr_service.dart | 1612 +++++++++++------ .../features/auth/data/dummy/basix_usage.dart | 488 ++--- .../authentication_repository.dart | 7 +- 4 files changed, 1318 insertions(+), 794 deletions(-) diff --git a/sigap-mobile/lib/main.dart b/sigap-mobile/lib/main.dart index 8181ba4..f9feb49 100644 --- a/sigap-mobile/lib/main.dart +++ b/sigap-mobile/lib/main.dart @@ -1,19 +1,22 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:get_storage/get_storage.dart'; import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart'; import 'package:sigap/app.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; Future main() async { - WidgetsFlutterBinding.ensureInitialized(); + final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); // Make sure status bar is properly set SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle(statusBarColor: Colors.transparent), ); + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + // Load environment variables from the .env file await dotenv.load(fileName: ".env"); diff --git a/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart b/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart index fdfd618..5d12123 100644 --- a/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart +++ b/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart @@ -3,6 +3,9 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:logger/logger.dart'; +import 'package:sigap/src/features/daily-ops/data/models/models/kta_model.dart'; +import 'package:sigap/src/features/personalization/data/models/models/ktp_model.dart'; import 'package:sigap/src/utils/constants/api_urls.dart'; import 'package:sigap/src/utils/dio.client/dio_client.dart'; @@ -53,7 +56,13 @@ class AzureOCRService { // Poll for results final ocrResult = await _pollForOcrResults(operationLocation); + + // Debug: LoggerLogger().i extracted content to help troubleshoot + Logger().i( + 'Full extracted text: ${ocrResult['analyzeResult']['content']}', + ); + // Parse the extracted information based on document type return isOfficer ? _extractKtaInfo(ocrResult) : _extractKtpInfo(ocrResult); @@ -63,6 +72,7 @@ class AzureOCRService { ); } } catch (e) { + Logger().i('OCR processing error: $e'); throw Exception('OCR processing error: $e'); } } @@ -84,13 +94,24 @@ class AzureOCRService { ); if (response.statusCode == 200) { - final result = json.decode(response.data); - final status = result['status']; + // Parse the data based on whether it's already a Map or a JSON string + final dynamic result = response.data; + Map resultMap; + + if (result is String) { + resultMap = json.decode(result); + } else if (result is Map) { + resultMap = result; + } else { + throw Exception('Unexpected response format from Azure OCR API'); + } + + final String status = resultMap['status']; if (status == 'succeeded') { - return result; + return resultMap; } else if (status == 'failed') { - throw Exception('OCR operation failed: ${result['error']}'); + throw Exception('OCR operation failed: ${resultMap['error']}'); } // If status is 'running' or 'notStarted', continue polling } else { @@ -112,620 +133,1121 @@ class AzureOCRService { throw Exception('Timeout while waiting for OCR results'); } - // Extract KTP (Civilian ID) information - updated for Read API response format + // Extract KTP (Civilian ID) information using Document Intelligence API format Map _extractKtpInfo(Map ocrResult) { final Map extractedInfo = {}; final List allLines = _getAllTextLinesFromReadAPI(ocrResult); + final String fullText = _getFullText(ocrResult); - // Print all lines for debugging - print('Extracted ${allLines.length} lines from KTP'); + // LoggerLogger().i raw extraction for debugging + Logger().i('Extracted ${allLines.length} lines from KTP'); for (int i = 0; i < allLines.length; i++) { - print('Line $i: ${allLines[i]}'); + Logger().i('Line $i: ${allLines[i]}'); } - // Creating a single concatenated text for regex-based extraction - final String fullText = allLines.join(' ').toLowerCase(); + // Extract NIK using various methods (label-based, regex patterns) + _extractNikFromKtp(extractedInfo, allLines, fullText); + + // Extract name + _extractNameFromKtp(extractedInfo, allLines, fullText); - // NIK extraction - Look for pattern "NIK: 1234567890123456" - RegExp nikRegex = RegExp(r'nik\s*:?\s*(\d{16})'); + // Extract birth place and date + _extractBirthInfoFromKtp(extractedInfo, allLines, fullText); + + // Extract gender + _extractGenderFromKtp(extractedInfo, allLines, fullText); + + // Extract address + _extractAddressFromKtp(extractedInfo, allLines, fullText); + + // Extract RT/RW information + _extractRtRwFromKtp(extractedInfo, allLines); + + // Extract kelurahan (village) information + _extractKelurahanFromKtp(extractedInfo, allLines); + + // Extract kecamatan (district) information + _extractKecamatanFromKtp(extractedInfo, allLines); + + // Extract religion + _extractReligionFromKtp(extractedInfo, allLines); + + // Extract marital status + _extractMaritalStatusFromKtp(extractedInfo, allLines); + + // Extract occupation + _extractOccupationFromKtp(extractedInfo, allLines); + + // Extract nationality + _extractNationalityFromKtp(extractedInfo, allLines); + + // Extract validity period + _extractValidityPeriodFromKtp(extractedInfo, allLines); + + // Extract blood type + _extractBloodTypeFromKtp(extractedInfo, allLines); + + // LoggerLogger().i extracted information for debugging + Logger().i('Extracted KTP info: ${extractedInfo.toString()}'); + + return extractedInfo; + } + + // Extract NIK from KTP + void _extractNikFromKtp( + Map extractedInfo, + List allLines, + String fullText, + ) { + // Try multiple approaches to find NIK + + // Approach 1: Using regex on full text + RegExp nikRegex = RegExp(r'nik\s*:?\s*(\d{16})', caseSensitive: false); var nikMatch = nikRegex.firstMatch(fullText); if (nikMatch != null && nikMatch.groupCount >= 1) { extractedInfo['nik'] = nikMatch.group(1)!; - } else { - // Try alternative format where NIK might be on a separate line - for (int i = 0; i < allLines.length; i++) { - if (allLines[i].toLowerCase().contains('nik')) { - // NIK label found, check next line or same line after colon - String line = allLines[i].toLowerCase(); - if (line.contains(':')) { - String potentialNik = line.split(':')[1].trim(); - // Clean and validate - potentialNik = potentialNik.replaceAll(RegExp(r'[^0-9]'), ''); - if (potentialNik.length == 16) { - extractedInfo['nik'] = potentialNik; - } - } else if (i + 1 < allLines.length) { - // Check next line - String potentialNik = allLines[i + 1].trim(); - // Clean and validate - potentialNik = potentialNik.replaceAll(RegExp(r'[^0-9]'), ''); - if (potentialNik.length == 16) { - extractedInfo['nik'] = potentialNik; - } + return; + } + + // Approach 2: Try to find NIK label and next line + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if (line.contains('nik')) { + // Check same line after colon + if (line.contains(':')) { + String nikPart = line.split(':')[1].trim(); + String numeric = nikPart.replaceAll(RegExp(r'[^0-9]'), ''); + if (numeric.length >= 16) { + extractedInfo['nik'] = numeric.substring(0, 16); + return; + } + } + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1]; + String numeric = nextLine.replaceAll(RegExp(r'[^0-9]'), ''); + if (numeric.length >= 16) { + extractedInfo['nik'] = numeric.substring(0, 16); + return; } } } } + + // Approach 3: Look for any 16-digit number in the document + RegExp anyNikRegex = RegExp(r'(\d{16})'); + var anyNikMatch = anyNikRegex.firstMatch(fullText); + if (anyNikMatch != null && anyNikMatch.groupCount >= 1) { + extractedInfo['nik'] = anyNikMatch.group(1)!; + } + } - // Name extraction - Look for pattern "Nama: JOHN DOE" + // Extract name from KTP + void _extractNameFromKtp( + Map extractedInfo, + List allLines, + String fullText, + ) { for (int i = 0; i < allLines.length; i++) { String line = allLines[i].toLowerCase(); - if (line.contains('nama') || line.trim() == 'nama:') { - // Found name label, extract value + if (line.contains('nama') && !line.contains('agama')) { + // Check same line after colon if (line.contains(':')) { String name = line.split(':')[1].trim(); if (name.isNotEmpty) { - extractedInfo['nama'] = name; - } else if (i + 1 < allLines.length) { - // Name might be on next line - extractedInfo['nama'] = allLines[i + 1].trim(); - } - } else if (i + 1 < allLines.length) { - // Name on next line - extractedInfo['nama'] = allLines[i + 1].trim(); - } - break; - } - } - - // Birth Place and Date - Look for pattern "Tempat/Tgl Lahir: JAKARTA, DD-MM-YYYY" - for (int i = 0; i < allLines.length; i++) { - String line = allLines[i].toLowerCase(); - if (line.contains('tempat') && line.contains('lahir')) { - // Birth info found, might contain place and date - String birthInfo = ''; - - if (line.contains(':')) { - birthInfo = line.split(':')[1].trim(); - } else if (i + 1 < allLines.length) { - birthInfo = allLines[i + 1].trim(); - } - - if (birthInfo.isNotEmpty) { - // Try to separate place and date - if (birthInfo.contains(',')) { - List parts = birthInfo.split(','); - if (parts.isNotEmpty) { - extractedInfo['birth_place'] = parts[0].trim(); - } - if (parts.length >= 2) { - // Extract date part - String datePart = parts[1].trim(); - RegExp dateRegex = RegExp( - r'(\d{1,2}[-/\s\.]\d{1,2}[-/\s\.]\d{2,4})', - ); - var dateMatch = dateRegex.firstMatch(datePart); - if (dateMatch != null) { - extractedInfo['birthDate'] = dateMatch.group(0)!; - extractedInfo['tanggal_lahir'] = dateMatch.group(0)!; - } - } - } else { - // No comma separation, try to extract date directly - RegExp dateRegex = RegExp( - r'(\d{1,2}[-/\s\.]\d{1,2}[-/\s\.]\d{2,4})', - ); - var dateMatch = dateRegex.firstMatch(birthInfo); - if (dateMatch != null) { - extractedInfo['tanggal_lahir'] = dateMatch.group(0)!; - extractedInfo['birthDate'] = dateMatch.group(0)!; - - // Extract birth place by removing the date part - String place = - birthInfo.replaceAll(dateMatch.group(0)!, '').trim(); - if (place.isNotEmpty) { - extractedInfo['birth_place'] = place; - } - } + extractedInfo['nama'] = _normalizeCase(name); + return; } } - break; - } - } - - // Gender extraction - Look for pattern "Jenis Kelamin: LAKI-LAKI" - for (int i = 0; i < allLines.length; i++) { - String line = allLines[i].toLowerCase(); - if (line.contains('jenis') && - (line.contains('kelamin') || line.contains('klamin'))) { - String gender = ''; - if (line.contains(':')) { - gender = line.split(':')[1].trim(); - } else if (i + 1 < allLines.length) { - gender = allLines[i + 1].trim(); - } - - if (gender.isNotEmpty) { - // Normalize gender - if (gender.contains('laki') || - gender.contains('pria') || - gender == 'l') { - extractedInfo['gender'] = 'Male'; - extractedInfo['jenis_kelamin'] = 'LAKI-LAKI'; - } else if (gender.contains('perempuan') || - gender.contains('wanita') || - gender == 'p') { - extractedInfo['gender'] = 'Female'; - extractedInfo['jenis_kelamin'] = 'PEREMPUAN'; - } else { - extractedInfo['gender'] = gender; - extractedInfo['jenis_kelamin'] = gender; + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + if (nextLine.isNotEmpty && !nextLine.toLowerCase().contains(':')) { + extractedInfo['nama'] = _normalizeCase(nextLine); + return; } } - break; } } - - // Blood Type - "Gol. Darah: A" - for (int i = 0; i < allLines.length; i++) { - String line = allLines[i].toLowerCase(); - if (line.contains('gol') && line.contains('darah')) { - String bloodType = ''; - if (line.contains(':')) { - bloodType = line.split(':')[1].trim(); - } else if (i + 1 < allLines.length) { - bloodType = allLines[i + 1].trim(); - } - - if (bloodType.isNotEmpty) { - // Normalize to just the letter (A, B, AB, O) - RegExp bloodTypeRegex = RegExp(r'([ABO]|AB)[-+]?'); - var bloodTypeMatch = bloodTypeRegex.firstMatch( - bloodType.toUpperCase(), - ); - if (bloodTypeMatch != null) { - extractedInfo['blood_type'] = bloodTypeMatch.group(0)!; - } else { - extractedInfo['blood_type'] = bloodType; - } - } - break; - } + + // Try to find name pattern in entire text + RegExp nameRegex = RegExp( + r'nama\s*:\s*([A-Za-z\s]+)', + caseSensitive: false, + ); + var nameMatch = nameRegex.firstMatch(fullText); + if (nameMatch != null && nameMatch.groupCount >= 1) { + extractedInfo['nama'] = _normalizeCase(nameMatch.group(1)!.trim()); } - - // Address extraction - for (int i = 0; i < allLines.length; i++) { - String line = allLines[i].toLowerCase(); - if (line.contains('alamat')) { - String address = ''; - if (line.contains(':')) { - address = line.split(':')[1].trim(); - } else if (i + 1 < allLines.length) { - address = allLines[i + 1].trim(); - } - - // Address might span multiple lines, collect until we hit another field - if (address.isNotEmpty) { - extractedInfo['address'] = address; - extractedInfo['alamat'] = address; - - // Try to collect additional address lines - int j = i + 2; // Start from two lines after 'alamat' - while (j < allLines.length) { - String nextLine = allLines[j].toLowerCase(); - // Stop if we encounter another field label - if (nextLine.contains(':') || - nextLine.contains('rt/rw') || - nextLine.contains('kel') || - nextLine.contains('kec')) { - break; - } - // Add this line to address - extractedInfo['address'] = - '${extractedInfo['address'] ?? ''} ${allLines[j].trim()}'; - extractedInfo['alamat'] = - '${extractedInfo['alamat'] ?? ''} ${allLines[j].trim()}'; - j++; - } - } - break; - } - } - - // RT/RW extraction - for (int i = 0; i < allLines.length; i++) { - String line = allLines[i].toLowerCase(); - if (line.contains('rt') && line.contains('rw')) { - String rtRw = ''; - if (line.contains(':')) { - rtRw = line.split(':')[1].trim(); - } else if (i + 1 < allLines.length) { - rtRw = allLines[i + 1].trim(); - } - - if (rtRw.isNotEmpty) { - extractedInfo['rt_rw'] = rtRw; - } - break; - } - } - - // Kel/Desa extraction - for (int i = 0; i < allLines.length; i++) { - String line = allLines[i].toLowerCase(); - if ((line.contains('kel') && line.contains('desa')) || - line.contains('kelurahan') || - line.contains('desa')) { - String kelDesa = ''; - if (line.contains(':')) { - kelDesa = line.split(':')[1].trim(); - } else if (i + 1 < allLines.length) { - kelDesa = allLines[i + 1].trim(); - } - - if (kelDesa.isNotEmpty) { - extractedInfo['kelurahan'] = kelDesa; - } - break; - } - } - - // Kecamatan extraction - for (int i = 0; i < allLines.length; i++) { - String line = allLines[i].toLowerCase(); - if (line.contains('kecamatan')) { - String kecamatan = ''; - if (line.contains(':')) { - kecamatan = line.split(':')[1].trim(); - } else if (i + 1 < allLines.length) { - kecamatan = allLines[i + 1].trim(); - } - - if (kecamatan.isNotEmpty) { - extractedInfo['kecamatan'] = kecamatan; - } - break; - } - } - - // Religion extraction - "Agama: ISLAM" - for (int i = 0; i < allLines.length; i++) { - String line = allLines[i].toLowerCase(); - if (line.contains('agama')) { - String religion = ''; - if (line.contains(':')) { - religion = line.split(':')[1].trim(); - } else if (i + 1 < allLines.length) { - religion = allLines[i + 1].trim(); - } - - if (religion.isNotEmpty) { - extractedInfo['religion'] = religion; - extractedInfo['agama'] = religion; - } - break; - } - } - - // Marital Status - "Status Perkawinan: BELUM KAWIN" - for (int i = 0; i < allLines.length; i++) { - String line = allLines[i].toLowerCase(); - if (line.contains('status') && line.contains('kawin')) { - String status = ''; - if (line.contains(':')) { - status = line.split(':')[1].trim(); - } else if (i + 1 < allLines.length) { - status = allLines[i + 1].trim(); - } - - if (status.isNotEmpty) { - extractedInfo['marital_status'] = status; - extractedInfo['status_perkawinan'] = status; - } - break; - } - } - - // Occupation - "Pekerjaan: KARYAWAN" - for (int i = 0; i < allLines.length; i++) { - String line = allLines[i].toLowerCase(); - if (line.contains('pekerjaan')) { - String occupation = ''; - if (line.contains(':')) { - occupation = line.split(':')[1].trim(); - } else if (i + 1 < allLines.length) { - occupation = allLines[i + 1].trim(); - } - - if (occupation.isNotEmpty) { - extractedInfo['occupation'] = occupation; - extractedInfo['pekerjaan'] = occupation; - } - break; - } - } - - // Nationality - "Kewarganegaraan: WNI" - for (int i = 0; i < allLines.length; i++) { - String line = allLines[i].toLowerCase(); - if (line.contains('kewarganegaraan')) { - String nationality = ''; - if (line.contains(':')) { - nationality = line.split(':')[1].trim(); - } else if (i + 1 < allLines.length) { - nationality = allLines[i + 1].trim(); - } - - if (nationality.isNotEmpty) { - extractedInfo['nationality'] = nationality; - extractedInfo['kewarganegaraan'] = nationality; - } - break; - } - } - - // Validity Period - "Berlaku Hingga: SEUMUR HIDUP" - for (int i = 0; i < allLines.length; i++) { - String line = allLines[i].toLowerCase(); - if (line.contains('berlaku') || line.contains('masa')) { - String validity = ''; - if (line.contains(':')) { - validity = line.split(':')[1].trim(); - } else if (i + 1 < allLines.length) { - validity = allLines[i + 1].trim(); - } - - if (validity.isNotEmpty) { - extractedInfo['validity_period'] = validity; - extractedInfo['berlaku_hingga'] = validity; - } - break; - } - } - - // Issue date extraction - RegExp issueDateRegex = RegExp(r'(\d{1,2}[-/\s\.]\d{1,2}[-/\s\.]\d{4})'); - for (int i = 0; i < allLines.length; i++) { - var match = issueDateRegex.firstMatch(allLines[i]); - if (match != null) { - String dateCandidate = match.group(0)!; - // Check if this is not already assigned as birth date - if (extractedInfo['birthDate'] != dateCandidate && - extractedInfo['tanggal_lahir'] != dateCandidate) { - extractedInfo['issue_date'] = dateCandidate; - break; - } - } - } - - // Print extracted info for debugging - print('Extracted KTP info: ${extractedInfo.toString()}'); - return extractedInfo; } - // Extract KTA (Officer ID) information - updated for Read API response format - Map _extractKtaInfo(Map ocrResult) { - final Map extractedInfo = {}; - final List allLines = _getAllTextLinesFromReadAPI(ocrResult); - + // Extract birth information + void _extractBirthInfoFromKtp( + Map extractedInfo, + List allLines, + String fullText, + ) { + // Common birth place/date patterns for (int i = 0; i < allLines.length; i++) { String line = allLines[i].toLowerCase(); - - // Extract name (usually prefixed with "Nama" or "Nama:") - if (line.contains('nama') && i + 1 < allLines.length) { - String name = - line.contains(':') - ? line.split(':')[1].trim() - : allLines[i + 1].trim(); - extractedInfo['nama'] = name; + + if ((line.contains('tempat') && line.contains('lahir')) || + line.contains('ttl') || + line.contains('tgl lahir')) { + // Check if birth info is on the same line after colon + if (line.contains(':')) { + String birthInfo = line.split(':')[1].trim(); + _processBirthInfo(extractedInfo, birthInfo); + return; + } + + // Check next line for birth info + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + _processBirthInfo(extractedInfo, nextLine); + return; + } } + } - // Extract Pangkat (Rank) - if (line.contains('pangkat') && i + 1 < allLines.length) { - String rank = - line.contains(':') - ? line.split(':')[1].trim() - : allLines[i + 1].trim(); - extractedInfo['pangkat'] = rank; + // Try to find birth info with regex + RegExp birthRegex = RegExp( + r'(?:tempat|tgl).+lahir\s*:\s*(.+)', + caseSensitive: false, + ); + var birthMatch = birthRegex.firstMatch(fullText); + if (birthMatch != null && birthMatch.groupCount >= 1) { + _processBirthInfo(extractedInfo, birthMatch.group(1)!.trim()); + } + } + + // Process birth information text + void _processBirthInfo(Map extractedInfo, String birthInfo) { + // Check if contains comma (indicating place, date format) + if (birthInfo.contains(',')) { + List parts = birthInfo.split(','); + if (parts.isNotEmpty) { + extractedInfo['birth_place'] = _normalizeCase(parts[0].trim()); + extractedInfo['birthPlace'] = _normalizeCase(parts[0].trim()); } - - // Extract NRP (ID Number for officers) - if ((line.contains('nrp') || line.contains('nip')) && - i + 1 < allLines.length) { - String nrp = - line.contains(':') - ? line.split(':')[1].trim() - : allLines[i + 1].trim(); - // Clean up the NRP (remove any non-alphanumeric characters) - nrp = nrp.replaceAll(RegExp(r'[^a-zA-Z0-9]'), ''); - extractedInfo['nrp'] = nrp; - } - - // Extract Unit - if (line.contains('unit') || - line.contains('kesatuan') && i + 1 < allLines.length) { - String unit = - line.contains(':') - ? line.split(':')[1].trim() - : allLines[i + 1].trim(); - extractedInfo['unit'] = unit; - } - - // Extract birth date (usually prefixed with "Tanggal Lahir" or similar) - if ((line.contains('lahir') || line.contains('ttl')) && - i + 1 < allLines.length) { - String birthInfo = - line.contains(':') - ? line.split(':')[1].trim() - : allLines[i + 1].trim(); - // Try to extract date in format DD-MM-YYYY or similar - RegExp dateRegex = RegExp(r'(\d{1,2}[-/\s\.]\d{1,2}[-/\s\.]\d{2,4})'); - var match = dateRegex.firstMatch(birthInfo); + + if (parts.length > 1) { + // Extract date from second part + RegExp dateRegex = RegExp(r'\d{1,2}[-/\s\.]\d{1,2}[-/\s\.]\d{2,4}'); + var match = dateRegex.firstMatch(parts[1]); if (match != null) { + extractedInfo['birthDate'] = match.group(0)!; extractedInfo['tanggal_lahir'] = match.group(0)!; } } + } else { + // Try to extract date from the string + RegExp dateRegex = RegExp(r'\d{1,2}[-/\s\.]\d{1,2}[-/\s\.]\d{2,4}'); + var match = dateRegex.firstMatch(birthInfo); + + if (match != null) { + String dateStr = match.group(0)!; + extractedInfo['birthDate'] = dateStr; + extractedInfo['tanggal_lahir'] = dateStr; + + // Extract birth place by removing the date + String place = birthInfo.replaceAll(dateStr, '').trim(); + if (place.isNotEmpty) { + extractedInfo['birth_place'] = _normalizeCase(place); + extractedInfo['birthPlace'] = _normalizeCase(place); + } + } else { + // If no date is found, consider the entire text as birth place + extractedInfo['birth_place'] = _normalizeCase(birthInfo); + extractedInfo['birthPlace'] = _normalizeCase(birthInfo); + } + } + } + + // Extract gender + void _extractGenderFromKtp( + Map extractedInfo, + List allLines, + String fullText, + ) { + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if ((line.contains('jenis') && line.contains('kelamin')) || + line.contains('gender')) { + // Extract from same line if contains colon + if (line.contains(':')) { + String genderText = line.split(':')[1].trim(); + _normalizeGender(extractedInfo, genderText); + return; + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + _normalizeGender(extractedInfo, nextLine); + return; + } + } } + // Try regex pattern + RegExp genderRegex = RegExp( + r'(?:jenis\s*kelamin|gender)\s*:\s*(.+?)(?:\s|$|:)', + caseSensitive: false, + ); + var match = genderRegex.firstMatch(fullText); + if (match != null && match.groupCount >= 1) { + _normalizeGender(extractedInfo, match.group(1)!.trim()); + } + } + + // Normalize gender text + void _normalizeGender(Map extractedInfo, String genderText) { + String gender = genderText.toLowerCase(); + + if (gender.contains('laki') || + gender.contains('pria') || + gender == 'l' || + gender == 'male') { + extractedInfo['gender'] = 'Male'; + extractedInfo['jenis_kelamin'] = 'LAKI-LAKI'; + } else if (gender.contains('perempuan') || + gender.contains('wanita') || + gender == 'p' || + gender == 'female') { + extractedInfo['gender'] = 'Female'; + extractedInfo['jenis_kelamin'] = 'PEREMPUAN'; + } else { + extractedInfo['gender'] = _normalizeCase(genderText); + extractedInfo['jenis_kelamin'] = _normalizeCase(genderText); + } + } + + // Extract address + void _extractAddressFromKtp( + Map extractedInfo, + List allLines, + String fullText, + ) { + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if (line.contains('alamat')) { + // Extract from same line if contains colon + if (line.contains(':')) { + String address = line.split(':')[1].trim(); + if (address.isNotEmpty) { + extractedInfo['address'] = _normalizeCase(address); + extractedInfo['alamat'] = _normalizeCase(address); + + // Try to collect additional lines that might be part of the address + _collectMultiLineAddress(extractedInfo, allLines, i + 1); + return; + } + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + if (nextLine.isNotEmpty && !_isLabelLine(nextLine)) { + extractedInfo['address'] = _normalizeCase(nextLine); + extractedInfo['alamat'] = _normalizeCase(nextLine); + + // Try to collect additional lines that might be part of the address + _collectMultiLineAddress(extractedInfo, allLines, i + 2); + return; + } + } + } + } + + // Try regex pattern + RegExp addressRegex = RegExp( + r'alamat\s*:\s*(.+?)(?=rt/rw|kel|kec|\n|$)', + caseSensitive: false, + ); + var match = addressRegex.firstMatch(fullText); + if (match != null && match.groupCount >= 1) { + String address = match.group(1)!.trim(); + extractedInfo['address'] = _normalizeCase(address); + extractedInfo['alamat'] = _normalizeCase(address); + } + } + + // Collect multi-line address + void _collectMultiLineAddress( + Map extractedInfo, + List allLines, + int startIndex, + ) { + if (!extractedInfo.containsKey('address')) return; + + String address = extractedInfo['address'] ?? ''; + String alamat = extractedInfo['alamat'] ?? ''; + + // Collect up to 3 lines or until we hit another field label + int maxLines = startIndex + 3; + for (int j = startIndex; j < allLines.length && j < maxLines; j++) { + String line = allLines[j].trim(); + if (_isLabelLine(line)) break; + + if (line.isNotEmpty) { + address += ' $line'; + alamat += ' $line'; + } + } + + extractedInfo['address'] = _normalizeCase(address); + extractedInfo['alamat'] = _normalizeCase(alamat); + } + + // Check if a line contains a label (ending with colon) + bool _isLabelLine(String line) { + return line.contains(':') || + RegExp( + r'(rt/rw|kel|kec|agama|status|pekerjaan|berlaku)', + ).hasMatch(line.toLowerCase()); + } + + // Extract RT/RW information + void _extractRtRwFromKtp( + Map extractedInfo, + List allLines, + ) { + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if (line.contains('rt') && line.contains('rw')) { + // Extract from same line if contains colon + if (line.contains(':')) { + String rtRw = line.split(':')[1].trim(); + extractedInfo['rt_rw'] = rtRw; + return; + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + if (nextLine.isNotEmpty && !_isLabelLine(nextLine)) { + extractedInfo['rt_rw'] = nextLine; + return; + } + } + } + } + } + + // Extract kelurahan information + void _extractKelurahanFromKtp( + Map extractedInfo, + List allLines, + ) { + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if ((line.contains('kel') && line.contains('desa')) || + line.contains('kelurahan') || + line.contains('desa')) { + + // Extract from same line if contains colon + if (line.contains(':')) { + String kelurahan = line.split(':')[1].trim(); + extractedInfo['kelurahan'] = _normalizeCase(kelurahan); + return; + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + if (nextLine.isNotEmpty && !_isLabelLine(nextLine)) { + extractedInfo['kelurahan'] = _normalizeCase(nextLine); + return; + } + } + } + } + } + + // Extract kecamatan information + void _extractKecamatanFromKtp( + Map extractedInfo, + List allLines, + ) { + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if (line.contains('kecamatan') || line.contains('kec')) { + // Extract from same line if contains colon + if (line.contains(':')) { + String kecamatan = line.split(':')[1].trim(); + extractedInfo['kecamatan'] = _normalizeCase(kecamatan); + return; + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + if (nextLine.isNotEmpty && !_isLabelLine(nextLine)) { + extractedInfo['kecamatan'] = _normalizeCase(nextLine); + return; + } + } + } + } + } + + // Extract religion information + void _extractReligionFromKtp( + Map extractedInfo, + List allLines, + ) { + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if (line.contains('agama')) { + + // Extract from same line if contains colon + if (line.contains(':')) { + String religion = line.split(':')[1].trim(); + extractedInfo['religion'] = _normalizeCase(religion); + extractedInfo['agama'] = _normalizeCase(religion); + return; + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + if (nextLine.isNotEmpty && !_isLabelLine(nextLine)) { + extractedInfo['religion'] = _normalizeCase(nextLine); + extractedInfo['agama'] = _normalizeCase(nextLine); + return; + } + } + } + } + } + + // Extract marital status information + void _extractMaritalStatusFromKtp( + Map extractedInfo, + List allLines, + ) { + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if (line.contains('status') && + (line.contains('kawin') || line.contains('perkawinan'))) { + // Extract from same line if contains colon + if (line.contains(':')) { + String status = line.split(':')[1].trim(); + extractedInfo['marital_status'] = _normalizeCase(status); + extractedInfo['status_perkawinan'] = _normalizeCase(status); + return; + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + if (nextLine.isNotEmpty && !_isLabelLine(nextLine)) { + extractedInfo['marital_status'] = _normalizeCase(nextLine); + extractedInfo['status_perkawinan'] = _normalizeCase(nextLine); + return; + } + } + } + } + } + + // Extract occupation information + void _extractOccupationFromKtp( + Map extractedInfo, + List allLines, + ) { + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if (line.contains('pekerjaan')) { + + // Extract from same line if contains colon + if (line.contains(':')) { + String occupation = line.split(':')[1].trim(); + extractedInfo['occupation'] = _normalizeCase(occupation); + extractedInfo['pekerjaan'] = _normalizeCase(occupation); + return; + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + if (nextLine.isNotEmpty && !_isLabelLine(nextLine)) { + extractedInfo['occupation'] = _normalizeCase(nextLine); + extractedInfo['pekerjaan'] = _normalizeCase(nextLine); + return; + } + } + } + } + } + + // Extract nationality information + void _extractNationalityFromKtp( + Map extractedInfo, + List allLines, + ) { + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if (line.contains('kewarganegaraan') || line.contains('nationality')) { + // Extract from same line if contains colon + if (line.contains(':')) { + String nationality = line.split(':')[1].trim(); + extractedInfo['nationality'] = _normalizeCase(nationality); + extractedInfo['kewarganegaraan'] = _normalizeCase(nationality); + return; + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + if (nextLine.isNotEmpty && !_isLabelLine(nextLine)) { + extractedInfo['nationality'] = _normalizeCase(nextLine); + extractedInfo['kewarganegaraan'] = _normalizeCase(nextLine); + return; + } + } + } + } + + // Default to WNI if not found explicitly + if (!extractedInfo.containsKey('nationality')) { + extractedInfo['nationality'] = 'WNI'; + extractedInfo['kewarganegaraan'] = 'WNI'; + } + } + + // Extract validity period information + void _extractValidityPeriodFromKtp( + Map extractedInfo, + List allLines, + ) { + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if ((line.contains('berlaku') && line.contains('hingga')) || + line.contains('masa') && line.contains('berlaku')) { + // Extract from same line if contains colon + if (line.contains(':')) { + String validity = line.split(':')[1].trim(); + extractedInfo['validity_period'] = _normalizeCase(validity); + extractedInfo['berlaku_hingga'] = _normalizeCase(validity); + return; + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + if (nextLine.isNotEmpty && !_isLabelLine(nextLine)) { + extractedInfo['validity_period'] = _normalizeCase(nextLine); + extractedInfo['berlaku_hingga'] = _normalizeCase(nextLine); + return; + } + } + } + } + } + + // Extract blood type information + void _extractBloodTypeFromKtp( + Map extractedInfo, + List allLines, + ) { + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if ((line.contains('gol') && line.contains('darah')) || + line.contains('blood')) { + // Extract from same line if contains colon + if (line.contains(':')) { + String bloodType = line.split(':')[1].trim(); + extractedInfo['blood_type'] = bloodType.toUpperCase(); + return; + } + + // Try to extract with regex for common blood types + RegExp bloodTypeRegex = RegExp(r'([ABO]|AB)[-+]?'); + var match = bloodTypeRegex.firstMatch(line); + if (match != null) { + extractedInfo['blood_type'] = match.group(0)!.toUpperCase(); + return; + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + if (nextLine.isNotEmpty && !_isLabelLine(nextLine)) { + extractedInfo['blood_type'] = nextLine.toUpperCase(); + return; + } + } + } + } + } + + // Extract KTA (Officer ID) information using Document Intelligence API format + Map _extractKtaInfo(Map ocrResult) { + final Map extractedInfo = {}; + final List allLines = _getAllTextLinesFromReadAPI(ocrResult); + final String fullText = _getFullText(ocrResult); + + // LoggerLogger().i raw extraction for debugging + Logger().i('Extracted ${allLines.length} lines from KTA'); + for (int i = 0; i < allLines.length; i++) { + Logger().i('Line $i: ${allLines[i]}'); + } + + // Extract officer name + _extractNameFromKta(extractedInfo, allLines, fullText); + + // Extract rank (pangkat) + _extractRankFromKta(extractedInfo, allLines, fullText); + + // Extract NRP (registration number) + _extractNrpFromKta(extractedInfo, allLines, fullText); + + // Extract police unit + _extractUnitFromKta(extractedInfo, allLines, fullText); + + // Extract card number (if available) + _extractCardNumberFromKta(extractedInfo, allLines, fullText); + + // Extract issue date (if available) + _extractIssueDateFromKta(extractedInfo, allLines); + + // LoggerLogger().i extracted information for debugging + Logger().i('Extracted KTA info: ${extractedInfo.toString()}'); + return extractedInfo; } - // Updated helper method to extract text lines from Read API result format + // Extract officer name from KTA + void _extractNameFromKta( + Map extractedInfo, + List allLines, + String fullText, + ) { + // First check for "Nama:" pattern + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if (line.contains('nama')) { + if (line.contains(':')) { + String name = line.split(':')[1].trim(); + if (name.isNotEmpty) { + extractedInfo['nama'] = _normalizeCase(name); + return; + } + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + if (nextLine.isNotEmpty && !_isKtaLabelLine(nextLine)) { + extractedInfo['nama'] = _normalizeCase(nextLine); + return; + } + } + } + } + + // If no explicit name field is found, try to extract from KTA card format + // In the example, name appears as the first proper name after header + if (allLines.length > 2) { + // Skip the header lines (KEPOLISIAN NEGARA... and KARTU TANDA ANGGOTA) + for (int i = 2; i < allLines.length; i++) { + String line = allLines[i].trim(); + // Check if line looks like a name (no numbers, not too short) + if (line.isNotEmpty && + !RegExp(r'\d').hasMatch(line) && + line.length > 3 && + !_isCommonKtaHeader(line)) { + extractedInfo['nama'] = _normalizeCase(line); + return; + } + } + } + } + + // Extract rank from KTA + void _extractRankFromKta( + Map extractedInfo, + List allLines, + String fullText, + ) { + // First check for "Pangkat:" pattern + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if (line.contains('pangkat')) { + if (line.contains(':')) { + String rank = line.split(':')[1].trim(); + extractedInfo['pangkat'] = _normalizeCase(rank); + return; + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + extractedInfo['pangkat'] = _normalizeCase(nextLine); + return; + } + } + } + + // In KTA cards, rank often appears near the name + // Look for common police ranks + List commonRanks = [ + 'BRIPTU', + 'BRIPKA', + 'AIPTU', + 'IPTU', + 'IPDA', + 'AKP', + 'KOMBES', + 'KOMBESPOL', + 'AKBP', + 'BRIGJEN', + 'IRJEN', + 'KOMJEN', + 'JENDERAL', + ]; + + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i]; + for (String rank in commonRanks) { + if (line.contains(rank)) { + extractedInfo['pangkat'] = rank; + return; + } + } + } + } + + // Extract NRP from KTA + void _extractNrpFromKta( + Map extractedInfo, + List allLines, + String fullText, + ) { + // First check for "NRP:" pattern + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if (line.contains('nrp') || line.contains('nip')) { + if (line.contains(':')) { + String nrp = line.split(':')[1].trim(); + String cleanNrp = nrp.replaceAll(RegExp(r'[^0-9]'), ''); + if (cleanNrp.isNotEmpty) { + extractedInfo['nrp'] = cleanNrp; + return; + } + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + String cleanNrp = nextLine.replaceAll(RegExp(r'[^0-9]'), ''); + if (cleanNrp.isNotEmpty) { + extractedInfo['nrp'] = cleanNrp; + return; + } + } + } + } + + // In KTA format, NRP is often a standalone 8-digit number + RegExp nrpRegex = RegExp(r'(\d{8}|\d{6})'); + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i]; + var match = nrpRegex.firstMatch(line); + if (match != null) { + extractedInfo['nrp'] = match.group(0)!; + return; + } + } + } + + // Extract unit from KTA + void _extractUnitFromKta( + Map extractedInfo, + List allLines, + String fullText, + ) { + // First check for "Unit:" or "Kesatuan:" pattern + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i].toLowerCase(); + if (line.contains('unit') || line.contains('kesatuan')) { + if (line.contains(':')) { + String unit = line.split(':')[1].trim(); + extractedInfo['unit'] = _normalizeCase(unit); + extractedInfo['kesatuan'] = _normalizeCase(unit); + return; + } + + // Check next line + if (i + 1 < allLines.length) { + String nextLine = allLines[i + 1].trim(); + extractedInfo['unit'] = _normalizeCase(nextLine); + extractedInfo['kesatuan'] = _normalizeCase(nextLine); + return; + } + } + } + + // Look for strings starting with "POLDA", "POLRES", etc. + RegExp policeUnitRegex = RegExp(r'(POLDA|POLRES|POLSEK|MABES)\s+[A-Z]+'); + var match = policeUnitRegex.firstMatch(fullText); + if (match != null) { + extractedInfo['unit'] = match.group(0)!; + extractedInfo['kesatuan'] = match.group(0)!; + return; + } + + // In the example KTA, "POLDA LAMPUNG" appears after the name and rank + for (int i = 0; i < allLines.length; i++) { + String line = allLines[i]; + if (line.contains('POLDA') || + line.contains('POLRES') || + line.contains('POLSEK') || + line.contains('MABES')) { + extractedInfo['unit'] = line.trim(); + extractedInfo['kesatuan'] = line.trim(); + return; + } + } + } + + // Extract card number from KTA + void _extractCardNumberFromKta( + Map extractedInfo, + List allLines, + String fullText, + ) { + // In the example KTA, card number is a set of 4 groups of 4 digits + RegExp cardNumberRegex = RegExp(r'(\d{4}\s+\d{4}\s+\d{4}\s+\d{4})'); + var match = cardNumberRegex.firstMatch(fullText); + if (match != null) { + extractedInfo['nomor_kartu'] = match + .group(0)! + .replaceAll(RegExp(r'\s+'), ' '); + extractedInfo['card_number'] = match + .group(0)! + .replaceAll(RegExp(r'\s+'), ' '); + } + } + + // Extract issue date from KTA + void _extractIssueDateFromKta( + Map extractedInfo, + List allLines, + ) { + // Look for date patterns (dd-mm-yyyy or similar) + RegExp dateRegex = RegExp(r'\d{1,2}[-/\s\.]\d{1,2}[-/\s\.]\d{2,4}'); + for (int i = 0; i < allLines.length; i++) { + var match = dateRegex.firstMatch(allLines[i]); + if (match != null) { + extractedInfo['tanggal_terbit'] = match.group(0)!; + extractedInfo['issue_date'] = match.group(0)!; + return; + } + } + } + + // Check if line is a common KTA header + bool _isCommonKtaHeader(String line) { + line = line.toUpperCase(); + return line.contains('KEPOLISIAN') || + line.contains('KARTU') || + line.contains('TANDA') || + line.contains('ANGGOTA') || + line.contains('REPUBLIK') || + line.contains('INDONESIA'); + } + + // Check if a line in KTA is a label line + bool _isKtaLabelLine(String line) { + return line.contains(':') || + RegExp( + r'(nrp|pangkat|kesatuan|jabatan|unit)', + ).hasMatch(line.toLowerCase()); + } + + // Get the full text from OCR result + String _getFullText(Map ocrResult) { + try { + // Check for both possible response formats + if (ocrResult.containsKey('analyzeResult') && + ocrResult['analyzeResult'].containsKey('content')) { + return ocrResult['analyzeResult']['content'] ?? ''; + } else if (ocrResult.containsKey('analyzeResult') && + ocrResult['analyzeResult'].containsKey('readResults')) { + // For older API format, concatenate all recognized text + final List readResults = + ocrResult['analyzeResult']['readResults']; + final StringBuffer buffer = StringBuffer(); + + for (var page in readResults) { + if (page.containsKey('lines')) { + for (var line in page['lines']) { + if (line.containsKey('text')) { + buffer.write('${line['text']} '); + } + } + } + } + return buffer.toString().trim(); + } + return ''; + } catch (e) { + Logger().i('Error getting full text: $e'); + return ''; + } + } + + // Helper method to extract all text lines from Read API response format List _getAllTextLinesFromReadAPI(Map ocrResult) { final List allText = []; try { - // Navigate through the Read API response structure + // LoggerLogger().i raw structure for debugging + Logger().i('OCR Result structure: ${ocrResult.keys}'); + + // Check if the response format uses readResults (v3.2 API) if (ocrResult.containsKey('analyzeResult') && ocrResult['analyzeResult'].containsKey('readResults')) { - final readResults = ocrResult['analyzeResult']['readResults']; + final List readResults = + ocrResult['analyzeResult']['readResults']; + Logger().i('Found ReadResults format with ${readResults.length} pages'); + for (var page in readResults) { if (page.containsKey('lines')) { - for (var line in page['lines']) { + final List lines = page['lines']; + Logger().i('Found ${lines.length} lines in page'); + + for (var line in lines) { if (line.containsKey('text')) { allText.add(line['text']); } } } } - } - } catch (e) { - print('Error extracting text from OCR result: $e'); - } - - return allText; - } - - // Original helper method for old OCR API format (keeping for compatibility) - List _getAllTextLines(Map ocrResult) { - final List allText = []; - - // Navigate through the OCR JSON structure to extract text - if (ocrResult.containsKey('regions')) { - for (var region in ocrResult['regions']) { - if (region.containsKey('lines')) { - for (var line in region['lines']) { - if (line.containsKey('words')) { - String lineText = ''; - for (var word in line['words']) { - if (word.containsKey('text')) { - lineText += word['text'] + ' '; - } - } - if (lineText.isNotEmpty) { - allText.add(lineText.trim()); + } + // Check if the response format uses pages (newer format) + else if (ocrResult.containsKey('analyzeResult') && + ocrResult['analyzeResult'].containsKey('pages')) { + final List pages = ocrResult['analyzeResult']['pages']; + Logger().i('Found Pages format with ${pages.length} pages'); + + for (var page in pages) { + if (page.containsKey('lines')) { + for (var line in page['lines']) { + if (line.containsKey('content')) { + allText.add(line['content']); } } } } } + // Handle any other potential formats + else if (ocrResult.containsKey('analyzeResult') && + ocrResult['analyzeResult'].containsKey('paragraphs')) { + final List paragraphs = + ocrResult['analyzeResult']['paragraphs']; + Logger().i( + 'Found Paragraphs format with ${paragraphs.length} paragraphs', + ); + + for (var paragraph in paragraphs) { + if (paragraph.containsKey('content')) { + allText.add(paragraph['content']); + } + } + } else { + Logger().i( + 'Unrecognized OCR result format. Keys available: ${ocrResult.keys}', + ); + if (ocrResult.containsKey('analyzeResult')) { + Logger().i('AnalyzeResult keys: ${ocrResult['analyzeResult'].keys}'); + } + } + + Logger().i('Extracted ${allText.length} text lines'); + + // As a fallback, if no lines were extracted and there's a content field, + // split content by newlines + if (allText.isEmpty && + ocrResult.containsKey('analyzeResult') && + ocrResult['analyzeResult'].containsKey('content')) { + String content = ocrResult['analyzeResult']['content']; + allText.addAll(content.split('\n')); + Logger().i('Used content fallback, extracted ${allText.length} lines'); + } + } catch (e) { + Logger().i('Error extracting text from OCR result: $e'); + Logger().i('OCR Result structure that caused error: ${ocrResult.keys}'); } return allText; } - // Validate if the extracted OCR data contains all required fields based on role - bool validateRequiredFields( - Map extractedInfo, - bool isOfficer, - ) { - List requiredFields = - isOfficer - ? ['nama', 'pangkat', 'nrp', 'unit', 'tanggal_lahir'] - : ['nik', 'nama', 'tanggal_lahir']; + // Normalize case for display (make first letter of each word uppercase) + String _normalizeCase(String text) { + if (text.isEmpty) return text; - // Check if all required fields are present and not empty - for (String field in requiredFields) { - if (!extractedInfo.containsKey(field) || extractedInfo[field]!.isEmpty) { - return false; - } + // If all uppercase or all lowercase, convert to title case + if (text == text.toUpperCase() || text == text.toLowerCase()) { + return text + .split(' ') + .map( + (word) => + word.isNotEmpty + ? '${word[0].toUpperCase()}${word.substring(1).toLowerCase()}' + : '', + ) + .join(' '); } - - // Check NIK format for KTP (must be at least 16 digits) - if (!isOfficer && extractedInfo.containsKey('nik')) { - String nik = extractedInfo['nik']!.replaceAll(RegExp(r'[^0-9]'), ''); - if (nik.length < 16) { - return false; - } - } - - // Check NRP format for KTA - if (isOfficer && extractedInfo.containsKey('nrp')) { - String nrp = extractedInfo['nrp']!; - if (nrp.length < 5) { - // Minimum expected length - return false; - } - } - - return true; + + // Otherwise keep as is (assuming it's already formatted correctly) + return text; } - // Get missing fields description for error message - String getMissingFieldsDescription( - Map extractedInfo, - bool isOfficer, - ) { - List missingFields = []; + // Helper to create KtpModel from extracted info + KtpModel createKtpModel(Map extractedInfo) { + return KtpModel( + nik: extractedInfo['nik'] ?? '', + name: extractedInfo['nama'] ?? '', + birthPlace: + extractedInfo['birthPlace'] ?? extractedInfo['birth_place'] ?? '', + birthDate: + extractedInfo['birthDate'] ?? extractedInfo['tanggal_lahir'] ?? '', + gender: extractedInfo['gender'] ?? extractedInfo['jenis_kelamin'] ?? '', + address: extractedInfo['alamat'] ?? extractedInfo['address'] ?? '', + nationality: + extractedInfo['nationality'] ?? + extractedInfo['kewarganegaraan'] ?? + 'WNI', + religion: extractedInfo['religion'] ?? extractedInfo['agama'], + occupation: extractedInfo['occupation'] ?? extractedInfo['pekerjaan'], + maritalStatus: + extractedInfo['marital_status'] ?? extractedInfo['status_perkawinan'], + bloodType: extractedInfo['blood_type'], + rtRw: extractedInfo['rt_rw'], + kelurahan: extractedInfo['kelurahan'], + kecamatan: extractedInfo['kecamatan'], + ); + } - if (isOfficer) { - // KTA required fields - if (!extractedInfo.containsKey('nama') || - extractedInfo['nama']!.isEmpty) { - missingFields.add('Name'); - } - - if (!extractedInfo.containsKey('pangkat') || - extractedInfo['pangkat']!.isEmpty) { - missingFields.add('Rank (Pangkat)'); - } - - if (!extractedInfo.containsKey('nrp') || extractedInfo['nrp']!.isEmpty) { - missingFields.add('NRP'); - } else { - String nrp = extractedInfo['nrp']!; - if (nrp.length < 5) { - missingFields.add('Valid NRP'); - } - } - - if (!extractedInfo.containsKey('unit') || - extractedInfo['unit']!.isEmpty) { - missingFields.add('Unit'); - } - - if (!extractedInfo.containsKey('tanggal_lahir') || - extractedInfo['tanggal_lahir']!.isEmpty) { - missingFields.add('Birth Date'); - } - } else { - // KTP required fields - if (!extractedInfo.containsKey('nik') || extractedInfo['nik']!.isEmpty) { - missingFields.add('NIK (Identity Number)'); - } else { - String nik = extractedInfo['nik']!.replaceAll(RegExp(r'[^0-9]'), ''); - if (nik.length < 16) { - missingFields.add('Valid NIK (should be 16 digits)'); - } - } - - if (!extractedInfo.containsKey('nama') || - extractedInfo['nama']!.isEmpty) { - missingFields.add('Name'); - } - - if (!extractedInfo.containsKey('tanggal_lahir') || - extractedInfo['tanggal_lahir']!.isEmpty) { - missingFields.add('Birth Date'); - } - - if (!extractedInfo.containsKey('alamat') || - extractedInfo['alamat']!.isEmpty) { - missingFields.add('Address'); - } - } - - return missingFields.join(', '); + // Helper to create KtaModel from extracted info + KtaModel createKtaModel(Map extractedInfo) { + return KtaModel( + name: extractedInfo['nama'] ?? '', + nrp: extractedInfo['nrp'] ?? '', + policeUnit: extractedInfo['unit'] ?? extractedInfo['kesatuan'] ?? '', + issueDate: + extractedInfo['issue_date'] ?? extractedInfo['tanggal_terbit'] ?? '', + cardNumber: + extractedInfo['card_number'] ?? extractedInfo['nomor_kartu'] ?? '', + extraData: { + 'pangkat': extractedInfo['pangkat'] ?? '', + 'tanggal_lahir': extractedInfo['tanggal_lahir'] ?? '', + }, + ); } // Process facial verification between ID card and selfie diff --git a/sigap-mobile/lib/src/features/auth/data/dummy/basix_usage.dart b/sigap-mobile/lib/src/features/auth/data/dummy/basix_usage.dart index 09e104c..624f6ca 100644 --- a/sigap-mobile/lib/src/features/auth/data/dummy/basix_usage.dart +++ b/sigap-mobile/lib/src/features/auth/data/dummy/basix_usage.dart @@ -1,286 +1,286 @@ -import 'dart:convert'; -import 'dart:io'; -import 'package:http/http.dart' as http; +// import 'dart:convert'; +// import 'dart:io'; +// import 'package:http/http.dart' as http; -class DocumentIntelligenceService { - final String endpoint; - final String key; - final http.Client _client = http.Client(); +// class DocumentIntelligenceService { +// final String endpoint; +// final String key; +// final http.Client _client = http.Client(); - DocumentIntelligenceService({ - required this.endpoint, - required this.key, - }); +// DocumentIntelligenceService({ +// required this.endpoint, +// required this.key, +// }); - // Generator function equivalent for getting text of spans - List getTextOfSpans(String content, List> spans) { - List textList = []; - for (var span in spans) { - int offset = span['offset']; - int length = span['length']; - textList.add(content.substring(offset, offset + length)); - } - return textList; - } +// // Generator function equivalent for getting text of spans +// List getTextOfSpans(String content, List> spans) { +// List textList = []; +// for (var span in spans) { +// int offset = span['offset']; +// int length = span['length']; +// textList.add(content.substring(offset, offset + length)); +// } +// return textList; +// } - Future> analyzeDocument(String documentUrl) async { - try { - // Initial request to start document analysis - final initialResponse = await _client.post( - Uri.parse('$endpoint/documentModels/prebuilt-read:analyze'), - headers: { - 'Content-Type': 'application/json', - 'Ocp-Apim-Subscription-Key': key, - }, - body: jsonEncode({ - 'urlSource': documentUrl, - }), - ); +// Future> analyzeDocument(String documentUrl) async { +// try { +// // Initial request to start document analysis +// final initialResponse = await _client.post( +// Uri.parse('$endpoint/documentModels/prebuilt-read:analyze'), +// headers: { +// 'Content-Type': 'application/json', +// 'Ocp-Apim-Subscription-Key': key, +// }, +// body: jsonEncode({ +// 'urlSource': documentUrl, +// }), +// ); - if (initialResponse.statusCode != 202) { - throw Exception('Failed to start analysis: ${initialResponse.body}'); - } +// if (initialResponse.statusCode != 202) { +// throw Exception('Failed to start analysis: ${initialResponse.body}'); +// } - // Get operation location from response headers - String? operationLocation = initialResponse.headers['operation-location']; - if (operationLocation == null) { - throw Exception('Operation location not found in response headers'); - } +// // Get operation location from response headers +// String? operationLocation = initialResponse.headers['operation-location']; +// if (operationLocation == null) { +// throw Exception('Operation location not found in response headers'); +// } - // Poll for results - return await _pollForResults(operationLocation); - } catch (e) { - throw Exception('Error analyzing document: $e'); - } - } +// // Poll for results +// return await _pollForResults(operationLocation); +// } catch (e) { +// throw Exception('Error analyzing document: $e'); +// } +// } - Future> _pollForResults(String operationLocation) async { - const int maxAttempts = 60; // Maximum polling attempts - const Duration pollInterval = Duration(seconds: 2); +// Future> _pollForResults(String operationLocation) async { +// const int maxAttempts = 60; // Maximum polling attempts +// const Duration pollInterval = Duration(seconds: 2); - for (int attempt = 0; attempt < maxAttempts; attempt++) { - final response = await _client.get( - Uri.parse(operationLocation), - headers: { - 'Ocp-Apim-Subscription-Key': key, - }, - ); +// for (int attempt = 0; attempt < maxAttempts; attempt++) { +// final response = await _client.get( +// Uri.parse(operationLocation), +// headers: { +// 'Ocp-Apim-Subscription-Key': key, +// }, +// ); - if (response.statusCode != 200) { - throw Exception('Failed to get operation status: ${response.body}'); - } +// if (response.statusCode != 200) { +// throw Exception('Failed to get operation status: ${response.body}'); +// } - final responseData = jsonDecode(response.body); - final String status = responseData['status']; +// final responseData = jsonDecode(response.body); +// final String status = responseData['status']; - if (status == 'succeeded') { - return responseData['analyzeResult']; - } else if (status == 'failed') { - throw Exception('Document analysis failed: ${responseData['error']}'); - } +// if (status == 'succeeded') { +// return responseData['analyzeResult']; +// } else if (status == 'failed') { +// throw Exception('Document analysis failed: ${responseData['error']}'); +// } - // Wait before next poll - await Future.delayed(pollInterval); - } +// // Wait before next poll +// await Future.delayed(pollInterval); +// } - throw Exception('Operation timed out after $maxAttempts attempts'); - } +// throw Exception('Operation timed out after $maxAttempts attempts'); +// } - Future processDocument(String documentUrl) async { - try { - final analyzeResult = await analyzeDocument(documentUrl); +// Future processDocument(String documentUrl) async { +// try { +// final analyzeResult = await analyzeDocument(documentUrl); - final String? content = analyzeResult['content']; - final List? pages = analyzeResult['pages']; - final List? languages = analyzeResult['languages']; - final List? styles = analyzeResult['styles']; +// final String? content = analyzeResult['content']; +// final List? pages = analyzeResult['pages']; +// final List? languages = analyzeResult['languages']; +// final List? styles = analyzeResult['styles']; - // Process pages - if (pages == null || pages.isEmpty) { - print('No pages were extracted from the document.'); - } else { - print('Pages:'); - for (var page in pages) { - print('- Page ${page['pageNumber']} (unit: ${page['unit']})'); - print(' ${page['width']}x${page['height']}, angle: ${page['angle']}'); +// // Process pages +// if (pages == null || pages.isEmpty) { +// print('No pages were extracted from the document.'); +// } else { +// print('Pages:'); +// for (var page in pages) { +// print('- Page ${page['pageNumber']} (unit: ${page['unit']})'); +// print(' ${page['width']}x${page['height']}, angle: ${page['angle']}'); - final List lines = page['lines'] ?? []; - final List words = page['words'] ?? []; +// final List lines = page['lines'] ?? []; +// final List words = page['words'] ?? []; - print(' ${lines.length} lines, ${words.length} words'); +// print(' ${lines.length} lines, ${words.length} words'); - if (lines.isNotEmpty) { - print(' Lines:'); - for (var line in lines) { - print(' - "${line['content']}"'); - } - } +// if (lines.isNotEmpty) { +// print(' Lines:'); +// for (var line in lines) { +// print(' - "${line['content']}"'); +// } +// } - if (words.isNotEmpty) { - print(' Words:'); - for (var word in words) { - print(' - "${word['content']}"'); - } - } - } - } +// if (words.isNotEmpty) { +// print(' Words:'); +// for (var word in words) { +// print(' - "${word['content']}"'); +// } +// } +// } +// } - // Process languages - if (languages == null || languages.isEmpty) { - print('No language spans were extracted from the document.'); - } else { - print('Languages:'); - for (var languageEntry in languages) { - print('- Found language: ${languageEntry['locale']} ' - '(confidence: ${languageEntry['confidence']})'); +// // Process languages +// if (languages == null || languages.isEmpty) { +// print('No language spans were extracted from the document.'); +// } else { +// print('Languages:'); +// for (var languageEntry in languages) { +// print('- Found language: ${languageEntry['locale']} ' +// '(confidence: ${languageEntry['confidence']})'); - if (content != null && languageEntry['spans'] != null) { - final spans = List>.from(languageEntry['spans']); - final textList = getTextOfSpans(content, spans); +// if (content != null && languageEntry['spans'] != null) { +// final spans = List>.from(languageEntry['spans']); +// final textList = getTextOfSpans(content, spans); - for (var text in textList) { - final escapedText = text - .replaceAll(RegExp(r'\r?\n'), '\\n') - .replaceAll('"', '\\"'); - print(' - "$escapedText"'); - } - } - } - } +// for (var text in textList) { +// final escapedText = text +// .replaceAll(RegExp(r'\r?\n'), '\\n') +// .replaceAll('"', '\\"'); +// print(' - "$escapedText"'); +// } +// } +// } +// } - // Process styles - if (styles == null || styles.isEmpty) { - print('No text styles were extracted from the document.'); - } else { - print('Styles:'); - for (var style in styles) { - final bool isHandwritten = style['isHandwritten'] ?? false; - final double confidence = style['confidence'] ?? 0.0; +// // Process styles +// if (styles == null || styles.isEmpty) { +// print('No text styles were extracted from the document.'); +// } else { +// print('Styles:'); +// for (var style in styles) { +// final bool isHandwritten = style['isHandwritten'] ?? false; +// final double confidence = style['confidence'] ?? 0.0; - print('- Handwritten: ${isHandwritten ? "yes" : "no"} ' - '(confidence=$confidence)'); +// print('- Handwritten: ${isHandwritten ? "yes" : "no"} ' +// '(confidence=$confidence)'); - if (content != null && style['spans'] != null) { - final spans = List>.from(style['spans']); - final textList = getTextOfSpans(content, spans); +// if (content != null && style['spans'] != null) { +// final spans = List>.from(style['spans']); +// final textList = getTextOfSpans(content, spans); - for (var text in textList) { - print(' - "$text"'); - } - } - } - } - } catch (e) { - print('An error occurred: $e'); - } - } +// for (var text in textList) { +// print(' - "$text"'); +// } +// } +// } +// } +// } catch (e) { +// print('An error occurred: $e'); +// } +// } - void dispose() { - _client.close(); - } -} +// void dispose() { +// _client.close(); +// } +// } -// Example usage in a Flutter app -class DocumentAnalyzerApp { - static const String endpoint = "YOUR_FORM_RECOGNIZER_ENDPOINT"; - static const String key = "YOUR_FORM_RECOGNIZER_KEY"; - static const String formUrl = - "https://raw.githubusercontent.com/Azure-Samples/cognitive-services-REST-api-samples/master/curl/form-recognizer/rest-api/read.png"; +// // Example usage in a Flutter app +// class DocumentAnalyzerApp { +// static const String endpoint = "YOUR_FORM_RECOGNIZER_ENDPOINT"; +// static const String key = "YOUR_FORM_RECOGNIZER_KEY"; +// static const String formUrl = +// "https://raw.githubusercontent.com/Azure-Samples/cognitive-services-REST-api-samples/master/curl/form-recognizer/rest-api/read.png"; - static Future main() async { - final service = DocumentIntelligenceService( - endpoint: endpoint, - key: key, - ); +// static Future main() async { +// final service = DocumentIntelligenceService( +// endpoint: endpoint, +// key: key, +// ); - try { - await service.processDocument(formUrl); - } finally { - service.dispose(); - } - } -} +// try { +// await service.processDocument(formUrl); +// } finally { +// service.dispose(); +// } +// } +// } -// Flutter Widget Example -import 'package:flutter/material.dart'; +// // Flutter Widget Example +// import 'package:flutter/material.dart'; -class DocumentAnalyzerWidget extends StatefulWidget { - const DocumentAnalyzerWidget({super.key}); +// class DocumentAnalyzerWidget extends StatefulWidget { +// const DocumentAnalyzerWidget({super.key}); - @override - _DocumentAnalyzerWidgetState createState() => _DocumentAnalyzerWidgetState(); -} +// @override +// _DocumentAnalyzerWidgetState createState() => _DocumentAnalyzerWidgetState(); +// } -class _DocumentAnalyzerWidgetState extends State { - final DocumentIntelligenceService _service = DocumentIntelligenceService( - endpoint: "YOUR_FORM_RECOGNIZER_ENDPOINT", - key: "YOUR_FORM_RECOGNIZER_KEY", - ); +// class _DocumentAnalyzerWidgetState extends State { +// final DocumentIntelligenceService _service = DocumentIntelligenceService( +// endpoint: "YOUR_FORM_RECOGNIZER_ENDPOINT", +// key: "YOUR_FORM_RECOGNIZER_KEY", +// ); - bool _isLoading = false; - String _result = ''; +// bool _isLoading = false; +// String _result = ''; - Future _analyzeDocument() async { - setState(() { - _isLoading = true; - _result = ''; - }); +// Future _analyzeDocument() async { +// setState(() { +// _isLoading = true; +// _result = ''; +// }); - try { - const String documentUrl = - "https://raw.githubusercontent.com/Azure-Samples/cognitive-services-REST-api-samples/master/curl/form-recognizer/rest-api/read.png"; +// try { +// const String documentUrl = +// "https://raw.githubusercontent.com/Azure-Samples/cognitive-services-REST-api-samples/master/curl/form-recognizer/rest-api/read.png"; - await _service.processDocument(documentUrl); - setState(() { - _result = 'Document analysis completed successfully!'; - }); - } catch (e) { - setState(() { - _result = 'Error: $e'; - }); - } finally { - setState(() { - _isLoading = false; - }); - } - } +// await _service.processDocument(documentUrl); +// setState(() { +// _result = 'Document analysis completed successfully!'; +// }); +// } catch (e) { +// setState(() { +// _result = 'Error: $e'; +// }); +// } finally { +// setState(() { +// _isLoading = false; +// }); +// } +// } - @override - void dispose() { - _service.dispose(); - super.dispose(); - } +// @override +// void dispose() { +// _service.dispose(); +// super.dispose(); +// } - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Document Intelligence'), - ), - body: Padding( - padding: EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - ElevatedButton( - onPressed: _isLoading ? null : _analyzeDocument, - child: _isLoading - ? CircularProgressIndicator(color: Colors.white) - : Text('Analyze Document'), - ), - SizedBox(height: 20), - if (_result.isNotEmpty) - Expanded( - child: SingleChildScrollView( - child: Text( - _result, - style: TextStyle(fontSize: 14), - ), - ), - ), - ], - ), - ), - ); - } -} \ No newline at end of file +// @override +// Widget build(BuildContext context) { +// return Scaffold( +// appBar: AppBar( +// title: Text('Document Intelligence'), +// ), +// body: Padding( +// padding: EdgeInsets.all(16.0), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.stretch, +// children: [ +// ElevatedButton( +// onPressed: _isLoading ? null : _analyzeDocument, +// child: _isLoading +// ? CircularProgressIndicator(color: Colors.white) +// : Text('Analyze Document'), +// ), +// SizedBox(height: 20), +// if (_result.isNotEmpty) +// Expanded( +// child: SingleChildScrollView( +// child: Text( +// _result, +// style: TextStyle(fontSize: 14), +// ), +// ), +// ), +// ], +// ), +// ), +// ); +// } +// } \ No newline at end of file diff --git a/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart b/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart index 4e828bb..6a34351 100644 --- a/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart +++ b/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:logger/logger.dart'; @@ -36,10 +37,8 @@ class AuthenticationRepository extends GetxController { // --------------------------------------------------------------------------- @override void onReady() { - // Delay the redirect to avoid issues during build - Future.delayed(Duration.zero, () { - screenRedirect(); - }); + FlutterNativeSplash.remove(); + screenRedirect(); } // Check for biometric login on app start