Created
June 29, 2025 04:19
-
-
Save 3bugs/8d1810e6dd1d83b1134c14b8e8f41108 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Coded by Promlert Lovichit, promlert@gmail.com, https://www.3bugs.com/ | |
| /* | |
| 🟢 Block: PASSPORT | |
| 🟡 Line: PASSPORT - Rect.fromLTRB(667.0, 262.0, 956.0, 308.0) | |
| 🟠 Element: PASSPORT - Rect.fromLTRB(667.0, 262.0, 956.0, 308.0) | |
| 🟢 Block: 5 | |
| 🟡 Line: 5 - Rect.fromLTRB(1178.0, 5.0, 1191.0, 31.0) | |
| 🟠 Element: 5 - Rect.fromLTRB(1178.0, 5.0, 1191.0, 31.0) | |
| 🟢 Block: Type /T | |
| 🟡 Line: Type /T - Rect.fromLTRB(1188.0, 275.0, 1373.0, 321.0) | |
| 🟠 Element: Type - Rect.fromLTRB(1188.0, 277.0, 1271.0, 321.0) | |
| 🟠 Element: /T - Rect.fromLTRB(1284.0, 275.0, 1373.0, 318.0) | |
| 🟢 Block: KINGDOM OF THAILAND | |
| 🟡 Line: KINGDOM OF THAILAND - Rect.fromLTRB(1190.0, 190.0, 1873.0, 238.0) | |
| 🟠 Element: KINGDOM - Rect.fromLTRB(1190.0, 190.0, 1474.0, 238.0) | |
| 🟠 Element: OF - Rect.fromLTRB(1492.0, 190.0, 1564.0, 238.0) | |
| 🟠 Element: THAILAND - Rect.fromLTRB(1583.0, 190.0, 1873.0, 238.0) | |
| 🟢 Block: P | |
| 🟡 Line: P - Rect.fromLTRB(1195.0, 333.0, 1228.0, 371.0) | |
| 🟠 Element: P - Rect.fromLTRB(1195.0, 333.0, 1228.0, 371.0) | |
| 🟢 Block: Surname / uuana | |
| 🟡 Line: Surname / uuana - Rect.fromLTRB(1191.0, 395.0, 1507.0, 438.0) | |
| 🟠 Element: Surname - Rect.fromLTRB(1191.0, 395.0, 1344.0, 433.0) | |
| 🟠 Element: / - Rect.fromLTRB(1355.0, 400.0, 1369.0, 434.0) | |
| 🟠 Element: uuana - Rect.fromLTRB(1382.0, 400.0, 1507.0, 438.0) | |
| 🟢 Block: LOVICHIT | |
| 🟡 Line: LOVICHIT - Rect.fromLTRB(1203.0, 451.0, 1440.0, 493.0) | |
| 🟠 Element: LOVICHIT - Rect.fromLTRB(1203.0, 451.0, 1440.0, 493.0) | |
| 🟢 Block: Title / Name / éwuno /d | |
| 🟡 Line: Title / Name / éwuno /d - Rect.fromLTRB(1196.0, 503.0, 1701.0, 553.0) | |
| 🟠 Element: Title - Rect.fromLTRB(1196.0, 508.0, 1271.0, 553.0) | |
| 🟠 Element: / - Rect.fromLTRB(1282.0, 508.0, 1299.0, 552.0) | |
| 🟠 Element: Name - Rect.fromLTRB(1310.0, 507.0, 1406.0, 552.0) | |
| 🟠 Element: / - Rect.fromLTRB(1420.0, 506.0, 1434.0, 550.0) | |
| 🟠 Element: éwuno - Rect.fromLTRB(1449.0, 505.0, 1635.0, 550.0) | |
| 🟠 Element: /d - Rect.fromLTRB(1653.0, 503.0, 1701.0, 548.0) | |
| 🟢 Block: Sex / | |
| 🟡 Line: Sex / - Rect.fromLTRB(1212.0, 868.0, 1291.0, 905.0) | |
| 🟠 Element: Sex - Rect.fromLTRB(1212.0, 868.0, 1262.0, 905.0) | |
| 🟠 Element: / - Rect.fromLTRB(1280.0, 868.0, 1291.0, 905.0) | |
| 🟢 Block: 8 | |
| 🟡 Line: 8 - Rect.fromLTRB(1668.0, 10.0, 1681.0, 30.0) | |
| 🟠 Element: 8 - Rect.fromLTRB(1668.0, 10.0, 1681.0, 30.0) | |
| 🟢 Block: Country Code /UsInH | |
| THA | |
| 🟡 Line: Country Code /UsInH - Rect.fromLTRB(1481.0, 280.0, 1866.0, 325.0) | |
| 🟠 Element: Country - Rect.fromLTRB(1481.0, 283.0, 1614.0, 325.0) | |
| 🟠 Element: Code - Rect.fromLTRB(1628.0, 281.0, 1712.0, 323.0) | |
| 🟠 Element: /UsInH - Rect.fromLTRB(1725.0, 280.0, 1866.0, 322.0) | |
| 🟡 Line: THA - Rect.fromLTRB(1488.0, 333.0, 1597.0, 384.0) | |
| 🟠 Element: THA - Rect.fromLTRB(1488.0, 333.0, 1597.0, 384.0) | |
| 🟢 Block: MISS NATPAPHAT | |
| Name in Thai /nnwnlne | |
| 🟡 Line: MISS NATPAPHAT - Rect.fromLTRB(1206.0, 568.0, 1668.0, 610.0) | |
| 🟠 Element: MISS - Rect.fromLTRB(1206.0, 568.0, 1322.0, 610.0) | |
| 🟠 Element: NATPAPHAT - Rect.fromLTRB(1347.0, 568.0, 1668.0, 610.0) | |
| 🟡 Line: Name in Thai /nnwnlne - Rect.fromLTRB(1209.0, 621.0, 1641.0, 668.0) | |
| 🟠 Element: Name - Rect.fromLTRB(1209.0, 624.0, 1299.0, 668.0) | |
| 🟠 Element: in - Rect.fromLTRB(1315.0, 623.0, 1340.0, 667.0) | |
| 🟠 Element: Thai - Rect.fromLTRB(1361.0, 623.0, 1430.0, 667.0) | |
| 🟠 Element: /nnwnlne - Rect.fromLTRB(1434.0, 621.0, 1641.0, 666.0) | |
| 🟢 Block: F | |
| 🟡 Line: F - Rect.fromLTRB(1210.0, 922.0, 1236.0, 961.0) | |
| 🟠 Element: F - Rect.fromLTRB(1210.0, 922.0, 1236.0, 961.0) | |
| 🟢 Block: ?L|, 10 | |
| 🟡 Line: ?L|, 10 - Rect.fromLTRB(1652.0, 9.0, 2021.0, 53.0) | |
| 🟠 Element: ?L|, - Rect.fromLTRB(1652.0, 11.0, 1974.0, 53.0) | |
| 🟠 Element: 10 - Rect.fromLTRB(1993.0, 10.0, 2021.0, 43.0) | |
| 🟢 Block: Nationality / šounâ Date of Birth / uLna | |
| THAI | |
| 🟡 Line: Nationality / šounâ Date of Birth / uLna - Rect.fromLTRB(1198.0, 745.0, 1929.0, 795.0) | |
| 🟠 Element: Nationality - Rect.fromLTRB(1198.0, 751.0, 1383.0, 795.0) | |
| 🟠 Element: / - Rect.fromLTRB(1394.0, 751.0, 1409.0, 793.0) | |
| 🟠 Element: šounâ - Rect.fromLTRB(1423.0, 749.0, 1532.0, 793.0) | |
| 🟠 Element: Date - Rect.fromLTRB(1589.0, 748.0, 1661.0, 791.0) | |
| 🟠 Element: of - Rect.fromLTRB(1676.0, 748.0, 1709.0, 790.0) | |
| 🟠 Element: Birth - Rect.fromLTRB(1722.0, 747.0, 1796.0, 790.0) | |
| 🟠 Element: / - Rect.fromLTRB(1809.0, 746.0, 1823.0, 788.0) | |
| 🟠 Element: uLna - Rect.fromLTRB(1837.0, 746.0, 1929.0, 789.0) | |
| 🟡 Line: THAI - Rect.fromLTRB(1207.0, 812.0, 1329.0, 850.0) | |
| 🟠 Element: THAI - Rect.fromLTRB(1207.0, 812.0, 1329.0, 850.0) | |
| 🟢 Block: Height / duga | |
| 1.58M | |
| 🟡 Line: Height / duga - Rect.fromLTRB(1200.0, 980.0, 1448.0, 1023.0) | |
| 🟠 Element: Height - Rect.fromLTRB(1200.0, 980.0, 1308.0, 1023.0) | |
| 🟠 Element: / - Rect.fromLTRB(1319.0, 980.0, 1335.0, 1023.0) | |
| 🟠 Element: duga - Rect.fromLTRB(1349.0, 980.0, 1448.0, 1023.0) | |
| 🟡 Line: 1.58M - Rect.fromLTRB(1213.0, 1031.0, 1340.0, 1076.0) | |
| 🟠 Element: 1.58M - Rect.fromLTRB(1213.0, 1031.0, 1340.0, 1076.0) | |
| 🟢 Block: aae of issue / umoan | |
| 🟡 Line: aae of issue / umoan - Rect.fromLTRB(1172.0, 1071.0, 1586.0, 1146.0) | |
| 🟠 Element: aae - Rect.fromLTRB(1172.0, 1087.0, 1290.0, 1146.0) | |
| 🟠 Element: of - Rect.fromLTRB(1290.0, 1085.0, 1326.0, 1140.0) | |
| 🟠 Element: issue - Rect.fromLTRB(1342.0, 1080.0, 1422.0, 1137.0) | |
| 🟠 Element: / - Rect.fromLTRB(1434.0, 1079.0, 1450.0, 1133.0) | |
| 🟠 Element: umoan - Rect.fromLTRB(1456.0, 1071.0, 1586.0, 1132.0) | |
| 🟢 Block: j8 FEB 2025 | |
| 🟡 Line: j8 FEB 2025 - Rect.fromLTRB(1211.0, 1143.0, 1518.0, 1190.0) | |
| 🟠 Element: j8 - Rect.fromLTRB(1211.0, 1149.0, 1256.0, 1190.0) | |
| 🟠 Element: FEB - Rect.fromLTRB(1283.0, 1147.0, 1374.0, 1188.0) | |
| 🟠 Element: 2025 - Rect.fromLTRB(1401.0, 1143.0, 1518.0, 1186.0) | |
| 🟢 Block: 18 OCT 2004 | |
| 🟡 Line: 18 OCT 2004 - Rect.fromLTRB(1601.0, 810.0, 1922.0, 849.0) | |
| 🟠 Element: 18 - Rect.fromLTRB(1601.0, 810.0, 1647.0, 849.0) | |
| 🟠 Element: OCT - Rect.fromLTRB(1672.0, 810.0, 1771.0, 849.0) | |
| 🟠 Element: 2004 - Rect.fromLTRB(1796.0, 810.0, 1922.0, 849.0) | |
| 🟢 Block: Place of Birth / a0uiñn | |
| BANGKOK | |
| 🟡 Line: Place of Birth / a0uiñn - Rect.fromLTRB(1590.0, 855.0, 2004.0, 903.0) | |
| 🟠 Element: Place - Rect.fromLTRB(1590.0, 862.0, 1674.0, 903.0) | |
| 🟠 Element: of - Rect.fromLTRB(1688.0, 861.0, 1722.0, 901.0) | |
| 🟠 Element: Birth - Rect.fromLTRB(1734.0, 859.0, 1808.0, 900.0) | |
| 🟠 Element: / - Rect.fromLTRB(1821.0, 859.0, 1836.0, 898.0) | |
| 🟠 Element: a0uiñn - Rect.fromLTRB(1849.0, 855.0, 2004.0, 898.0) | |
| 🟡 Line: BANGKOK - Rect.fromLTRB(1602.0, 918.0, 1850.0, 957.0) | |
| 🟠 Element: BANGKOK - Rect.fromLTRB(1602.0, 918.0, 1850.0, 957.0) | |
| 🟢 Block: Date of Expiry /uivunenų | |
| 17 FEB 2030 | |
| 🟡 Line: Date of Expiry /uivunenų - Rect.fromLTRB(1202.0, 1197.0, 1669.0, 1245.0) | |
| 🟠 Element: Date - Rect.fromLTRB(1202.0, 1202.0, 1275.0, 1245.0) | |
| 🟠 Element: of - Rect.fromLTRB(1290.0, 1201.0, 1324.0, 1243.0) | |
| 🟠 Element: Expiry - Rect.fromLTRB(1337.0, 1199.0, 1438.0, 1243.0) | |
| 🟠 Element: /uivunenų - Rect.fromLTRB(1451.0, 1197.0, 1669.0, 1242.0) | |
| 🟡 Line: 17 FEB 2030 - Rect.fromLTRB(1211.0, 1257.0, 1520.0, 1303.0) | |
| 🟠 Element: 17 - Rect.fromLTRB(1211.0, 1262.0, 1253.0, 1303.0) | |
| 🟠 Element: FEB - Rect.fromLTRB(1278.0, 1260.0, 1369.0, 1301.0) | |
| 🟠 Element: 2030 - Rect.fromLTRB(1397.0, 1257.0, 1520.0, 1300.0) | |
| 🟢 Block: P<THALOVICHIT<<NATPAPHAT< | |
| 🟡 Line: P<THALOVICHIT<<NATPAPHAT< - Rect.fromLTRB(505.0, 1492.0, 1808.0, 1588.0) | |
| 🟠 Element: P<THALOVICHIT<<NATPAPHAT< - Rect.fromLTRB(505.0, 1492.0, 1808.0, 1588.0) | |
| 🟢 Block: 11 | |
| 🟡 Line: 11 - Rect.fromLTRB(2145.0, 14.0, 2172.0, 39.0) | |
| 🟠 Element: 11 - Rect.fromLTRB(2145.0, 14.0, 2172.0, 39.0) | |
| 🟢 Block: 12 | |
| 🟡 Line: 12 - Rect.fromLTRB(2308.0, 15.0, 2337.0, 35.0) | |
| 🟠 Element: 12 - Rect.fromLTRB(2308.0, 15.0, 2337.0, 35.0) | |
| 🟢 Block: nvƏnnỗnslns | |
| 🟡 Line: nvƏnnỗnslns - Rect.fromLTRB(2082.0, 156.0, 2569.0, 241.0) | |
| 🟠 Element: nvƏnnỗnslns - Rect.fromLTRB(2082.0, 156.0, 2569.0, 241.0) | |
| 🟢 Block: Passport No. / wăsốoâuwnaav | |
| AC8036237 | |
| 🟡 Line: Passport No. / wăsốoâuwnaav - Rect.fromLTRB(2077.0, 265.0, 2625.0, 321.0) | |
| 🟠 Element: Passport - Rect.fromLTRB(2077.0, 274.0, 2216.0, 321.0) | |
| 🟠 Element: No. - Rect.fromLTRB(2230.0, 273.0, 2282.0, 317.0) | |
| 🟠 Element: / - Rect.fromLTRB(2295.0, 272.0, 2311.0, 315.0) | |
| 🟠 Element: wăsốoâuwnaav - Rect.fromLTRB(2323.0, 264.0, 2625.0, 315.0) | |
| 🟡 Line: AC8036237 - Rect.fromLTRB(2087.0, 333.0, 2377.0, 378.0) | |
| 🟠 Element: AC8036237 - Rect.fromLTRB(2087.0, 333.0, 2377.0, 378.0) | |
| 🟢 Block: 14 | |
| 🟡 Line: 14 - Rect.fromLTRB(2628.0, 22.0, 2657.0, 39.0) | |
| 🟠 Element: 14 - Rect.fromLTRB(2628.0, 22.0, 2657.0, 39.0) | |
| 🟢 Block: ldentifcation No. / LavUsiänUsU | |
| 1103703873458 | |
| 🟡 Line: ldentifcation No. / LavUsiänUsU - Rect.fromLTRB(2074.0, 743.0, 2748.0, 785.0) | |
| 🟠 Element: ldentifcation - Rect.fromLTRB(2074.0, 743.0, 2292.0, 785.0) | |
| 🟠 Element: No. - Rect.fromLTRB(2308.0, 743.0, 2359.0, 785.0) | |
| 🟠 Element: / - Rect.fromLTRB(2373.0, 743.0, 2389.0, 785.0) | |
| 🟠 Element: LavUsiänUsU - Rect.fromLTRB(2403.0, 743.0, 2748.0, 785.0) | |
| 🟡 Line: 1103703873458 - Rect.fromLTRB(2083.0, 806.0, 2464.0, 846.0) | |
| 🟠 Element: 1103703873458 - Rect.fromLTRB(2083.0, 806.0, 2464.0, 846.0) | |
| 🟢 Block: Issuing Authority / Đanllae | |
| MINISTRY OF | |
| FOREIGN AFFAIRS | |
| Holder's Signature / | |
| anuñovognowuadoi@uwns | |
| 🟡 Line: Issuing Authority / Đanllae - Rect.fromLTRB(2073.0, 965.0, 2544.0, 1019.0) | |
| 🟠 Element: Issuing - Rect.fromLTRB(2073.0, 972.0, 2177.0, 1019.0) | |
| 🟠 Element: Authority - Rect.fromLTRB(2191.0, 969.0, 2349.0, 1017.0) | |
| 🟠 Element: / - Rect.fromLTRB(2362.0, 968.0, 2376.0, 1013.0) | |
| 🟠 Element: Đanllae - Rect.fromLTRB(2391.0, 965.0, 2544.0, 1013.0) | |
| 🟡 Line: MINISTRY OF - Rect.fromLTRB(2083.0, 1027.0, 2410.0, 1066.0) | |
| 🟠 Element: MINISTRY - Rect.fromLTRB(2083.0, 1027.0, 2317.0, 1066.0) | |
| 🟠 Element: OF - Rect.fromLTRB(2344.0, 1027.0, 2410.0, 1066.0) | |
| 🟡 Line: FOREIGN AFFAIRS - Rect.fromLTRB(2083.0, 1081.0, 2541.0, 1121.0) | |
| 🟠 Element: FOREIGN - Rect.fromLTRB(2083.0, 1081.0, 2299.0, 1121.0) | |
| 🟠 Element: AFFAIRS - Rect.fromLTRB(2325.0, 1081.0, 2541.0, 1121.0) | |
| 🟡 Line: Holder's Signature / - Rect.fromLTRB(2074.0, 1132.0, 2424.0, 1172.0) | |
| 🟠 Element: Holder's - Rect.fromLTRB(2074.0, 1132.0, 2218.0, 1169.0) | |
| 🟠 Element: Signature - Rect.fromLTRB(2234.0, 1135.0, 2397.0, 1172.0) | |
| 🟠 Element: / - Rect.fromLTRB(2410.0, 1138.0, 2424.0, 1172.0) | |
| 🟡 Line: anuñovognowuadoi@uwns - Rect.fromLTRB(2075.0, 1171.0, 2508.0, 1217.0) | |
| 🟠 Element: anuñovognowuadoi@uwns - Rect.fromLTRB(2075.0, 1171.0, 2508.0, 1217.0) | |
| 🟢 Block: AC80362379THA0410184 F30021751103703873458<86 | |
| 🟡 Line: AC80362379THA0410184 F30021751103703873458<86 - Rect.fromLTRB(545.0, 1602.0, 2787.0, 1720.0) | |
| 🟠 Element: AC80362379THA0410184 - Rect.fromLTRB(545.0, 1626.0, 1559.0, 1720.0) | |
| 🟠 Element: F30021751103703873458<86 - Rect.fromLTRB(1587.0, 1602.0, 2787.0, 1700.0) | |
| 🟢 Block: 15 | |
| 🟡 Line: 15 - Rect.fromLTRB(2791.0, 23.0, 2819.0, 51.0) | |
| 🟠 Element: 15 - Rect.fromLTRB(2791.0, 23.0, 2819.0, 51.0) | |
| 🟢 Block: 17 | |
| 🟡 Line: 17 - Rect.fromLTRB(3109.0, 26.0, 3136.0, 46.0) | |
| 🟠 Element: 17 - Rect.fromLTRB(3109.0, 26.0, 3136.0, 46.0) | |
| */ | |
| import 'dart:io'; | |
| import 'package:collection/collection.dart'; | |
| import 'package:flutter/foundation.dart'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:image_picker/image_picker.dart'; | |
| import 'package:lucide_icons_flutter/lucide_icons.dart'; | |
| import 'package:path_provider/path_provider.dart'; | |
| import 'package:user_registration/core/constants/ui_constants.dart'; | |
| import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; | |
| import 'package:image/image.dart' as img; | |
| import 'package:path/path.dart' as p; | |
| import 'package:user_registration/core/exceptions/app_exception.dart'; | |
| enum PassportDataType { | |
| headerKingdomOfThailand, | |
| headerKingdom, | |
| headerOf, | |
| headerThailand, | |
| headerPassport, | |
| labelPassportNumber, | |
| passportNumber, | |
| labelSurname, | |
| surname, | |
| labelTitleAndName, | |
| titleAndName, | |
| labelDateOfBirth, | |
| dateOfBirth, | |
| labelIdNumber, | |
| labelIdNumber2, | |
| idNumber, | |
| labelNationality, | |
| nationality, | |
| labelSex, | |
| sex, | |
| labelDateOfExpiry, | |
| dateOfExpiry, | |
| labelNameInThai, | |
| labelPlaceOfBirth, | |
| labelHeight, | |
| } | |
| const headerPassportRect = Rect.fromLTRB(667.0, 262.0, 956.0, 308.0); | |
| const headerThailandRect = Rect.fromLTRB(1583.0, 190.0, 1873.0, 238.0); | |
| const Map<PassportDataType, Rect> passportDataRects = { | |
| PassportDataType.headerKingdomOfThailand: | |
| Rect.fromLTRB(1190.0, 190.0, 1873.0, 238.0), | |
| PassportDataType.headerKingdom: Rect.fromLTRB(1190.0, 190.0, 1474.0, 238.0), | |
| PassportDataType.headerOf: Rect.fromLTRB(1492.0, 190.0, 1564.0, 238.0), | |
| PassportDataType.headerThailand: headerThailandRect, | |
| PassportDataType.headerPassport: headerPassportRect, | |
| PassportDataType.labelPassportNumber: | |
| Rect.fromLTRB(2077.0, 265.0, 2625.0, 321.0), | |
| PassportDataType.passportNumber: Rect.fromLTRB(2087.0, 333.0, 2377.0, 378.0), | |
| PassportDataType.labelSurname: Rect.fromLTRB(1191.0, 395.0, 1344.0, 433.0), | |
| PassportDataType.surname: Rect.fromLTRB(1203.0, 451.0, 1440.0, 493.0), | |
| PassportDataType.labelTitleAndName: | |
| Rect.fromLTRB(1196.0, 503.0, 1701.0, 553.0), | |
| PassportDataType.titleAndName: Rect.fromLTRB(1206.0, 568.0, 1668.0, 610.0), | |
| PassportDataType.labelDateOfBirth: | |
| Rect.fromLTRB(1589.0, 748.0, 1661.0, 791.0), | |
| PassportDataType.dateOfBirth: Rect.fromLTRB(1601.0, 810.0, 1922.0, 849.0), | |
| PassportDataType.labelIdNumber: Rect.fromLTRB(2074.0, 743.0, 2748.0, 785.0), | |
| PassportDataType.idNumber: Rect.fromLTRB(2083.0, 806.0, 2464.0, 846.0), | |
| PassportDataType.labelNationality: | |
| Rect.fromLTRB(1198.0, 751.0, 1383.0, 795.0), | |
| PassportDataType.nationality: Rect.fromLTRB(1207.0, 812.0, 1329.0, 850.0), | |
| PassportDataType.labelSex: Rect.fromLTRB(1212.0, 868.0, 1291.0, 905.0), | |
| PassportDataType.sex: Rect.fromLTRB(1210.0, 922.0, 1236.0, 961.0), | |
| PassportDataType.labelDateOfExpiry: | |
| Rect.fromLTRB(1202.0, 1197.0, 1669.0, 1245.0), | |
| PassportDataType.dateOfExpiry: Rect.fromLTRB(1211.0, 1257.0, 1520.0, 1303.0), | |
| }; | |
| const Map<PassportDataType, String> passportDataLabels = { | |
| PassportDataType.headerKingdomOfThailand: 'KINGDOM OF THAILAND', | |
| PassportDataType.headerKingdom: 'KINGDOM', | |
| PassportDataType.headerOf: 'OF', | |
| PassportDataType.headerThailand: 'THAILAND', | |
| PassportDataType.headerPassport: 'PASSPORT', | |
| PassportDataType.labelPassportNumber: 'Pa', // Passport No. | |
| PassportDataType.labelSurname: 'Su', // Surname | |
| PassportDataType.labelTitleAndName: 'Ti', // Title / Name | |
| PassportDataType.labelNationality: 'Na', | |
| PassportDataType.labelDateOfBirth: 'Da', | |
| PassportDataType.labelIdNumber: 'Id', // Identification No. | |
| PassportDataType.labelIdNumber2: 'ld', // Identification No. | |
| PassportDataType.labelSex: 'Se', | |
| PassportDataType.labelDateOfExpiry: 'Da', | |
| PassportDataType.labelNameInThai: 'Na', // Name in Thai | |
| PassportDataType.labelPlaceOfBirth: 'Pl', // Place of Birth | |
| PassportDataType.labelHeight: 'He', // Height | |
| }; | |
| const startWithLabels = [ | |
| PassportDataType.labelPassportNumber, | |
| PassportDataType.labelSurname, | |
| PassportDataType.labelTitleAndName, | |
| PassportDataType.labelNationality, | |
| PassportDataType.labelDateOfBirth, | |
| PassportDataType.labelIdNumber, | |
| PassportDataType.labelIdNumber2, | |
| PassportDataType.labelSex, | |
| PassportDataType.labelDateOfExpiry, | |
| PassportDataType.labelNameInThai, | |
| PassportDataType.labelPlaceOfBirth, | |
| PassportDataType.labelHeight, | |
| ]; | |
| // Top-level function for compute | |
| Future<Uint8List> _enhanceImageBytes(Uint8List bytes) async { | |
| img.Image? image = img.decodeImage(bytes); | |
| if (image == null) return bytes; | |
| // Increase contrast: 100 = no change, >100 = more contrast | |
| image = img.contrast(image, contrast: 110); | |
| image = img.grayscale(image); | |
| // image = img.binarize(image, threshold: 128); | |
| return Uint8List.fromList(img.encodeJpg(image)); | |
| } | |
| class PassportImageUtils { | |
| static double get refDistance => | |
| (headerPassportRect.topLeft - headerThailandRect.topRight).distance; | |
| static double calculateDistanceRatio( | |
| Rect imgHeaderPassportRect, Rect imgHeaderThailandRect) { | |
| return ((imgHeaderPassportRect.topLeft - imgHeaderThailandRect.topRight) | |
| .distance) / | |
| refDistance; | |
| } | |
| } | |
| class RecognizedPassport { | |
| final XFile image; | |
| final List<TextLine> lines; | |
| final TextLine passportHeader; | |
| final TextElement thailandHeader; | |
| final double distanceRatio; | |
| RecognizedPassport._({ | |
| required this.image, | |
| required this.lines, | |
| required this.passportHeader, | |
| required this.thailandHeader, | |
| required this.distanceRatio, | |
| }); | |
| static Future<RecognizedPassport> create(XFile image) async { | |
| final inputImage = InputImage.fromFilePath(image.path); | |
| final textRecognizer = TextRecognizer(script: TextRecognitionScript.latin); | |
| List<TextLine> lines = []; | |
| try { | |
| final RecognizedText recognizedText = | |
| await textRecognizer.processImage(inputImage); | |
| _logRecognizedText(recognizedText); | |
| // Get all lines, trim, and ignore empty lines | |
| lines = recognizedText.blocks | |
| .expand((block) => block.lines) | |
| .where((line) => line.text.trim().isNotEmpty) | |
| .toList(); | |
| // Remove all lines that start with startWithLabels | |
| lines = lines.where((line) { | |
| final trimmedText = line.text.trim(); | |
| return !startWithLabels | |
| .any((label) => trimmedText.startsWith(passportDataLabels[label]!)); | |
| }).toList(); | |
| // Debug print all text lines with their bounding boxes | |
| for (TextLine line in lines) { | |
| debugPrint('🟡 Line: ${line.text} - ${line.boundingBox}'); | |
| } | |
| } catch (e) { | |
| debugPrint('Error extracting passport info: $e'); | |
| } finally { | |
| textRecognizer.close(); | |
| } | |
| if (lines.isEmpty) { | |
| throw const AppException(message: 'ไม่พบข้อความใดๆในรูปภาพพาสปอร์ต'); | |
| } | |
| // Find text line containing "THAILAND" | |
| final kingdomOfThailandHeader = lines.firstWhereOrNull( | |
| (line) => line.text | |
| .trim() | |
| .contains(passportDataLabels[PassportDataType.headerThailand]!), | |
| ); | |
| if (kingdomOfThailandHeader == null) { | |
| throw AppException( | |
| message: 'ไม่พบข้อความ ' | |
| '"${passportDataLabels[PassportDataType.headerThailand]}" ' | |
| 'ในรูปภาพพาสปอร์ต'); | |
| } | |
| // Find text element for "THAILAND" | |
| final thailandHeader = kingdomOfThailandHeader.elements.firstWhereOrNull( | |
| (element) => | |
| element.text.trim() == | |
| passportDataLabels[PassportDataType.headerThailand]!, | |
| ); | |
| if (thailandHeader == null) { | |
| throw AppException( | |
| message: 'ไม่พบข้อความ ' | |
| '"${passportDataLabels[PassportDataType.headerThailand]}" ' | |
| 'ในรูปภาพพาสปอร์ต'); | |
| } | |
| // Find text line for the passport header | |
| final passportHeader = lines.firstWhereOrNull( | |
| (line) => | |
| line.text.trim() == | |
| passportDataLabels[PassportDataType.headerPassport]!, | |
| ); | |
| if (passportHeader == null) { | |
| throw AppException( | |
| message: 'ไม่พบข้อความ ' | |
| '"${passportDataLabels[PassportDataType.headerPassport]}" ' | |
| 'ในรูปภาพพาสปอร์ต'); | |
| } | |
| final distanceRatio = PassportImageUtils.calculateDistanceRatio( | |
| passportHeader.boundingBox, | |
| thailandHeader.boundingBox, | |
| ); | |
| return RecognizedPassport._( | |
| image: image, | |
| lines: lines, | |
| passportHeader: passportHeader, | |
| thailandHeader: thailandHeader, | |
| distanceRatio: distanceRatio, | |
| ); | |
| } | |
| static void _logRecognizedText(RecognizedText recognizedText) { | |
| String msg = ''; | |
| for (TextBlock block in recognizedText.blocks) { | |
| msg += '🟢 Block: ${block.text}\n'; | |
| // final Rect rect = block.boundingBox; | |
| // msg += | |
| // '- Rect: ${rect.left}, ${rect.top}, ${rect.right}, ${rect.bottom}\n'; | |
| // final List<Point<int>> cornerPoints = block.cornerPoints; | |
| // msg += | |
| // '- Corner Points: ${cornerPoints.map((p) => '(${p.x}, ${p.y})').join(', ')}\n'; | |
| // final List<String> languages = block.recognizedLanguages; | |
| // msg += '- Languages: ${languages.join(', ')}\n'; | |
| for (TextLine line in block.lines) { | |
| msg += ' 🟡 Line: ${line.text} - ${line.boundingBox}\n'; | |
| for (TextElement element in line.elements) { | |
| msg += ' 🟠 Element: ${element.text} - ${element.boundingBox}\n'; | |
| } | |
| } | |
| } | |
| debugPrint('Extracted Text: $msg'); | |
| } | |
| double _getDistanceFromPassportHeader(PassportDataType dataType) { | |
| return (passportDataRects[dataType]!.topLeft - headerPassportRect.topLeft) | |
| .distance; | |
| } | |
| double _getDistanceFromThailandHeader(PassportDataType dataType) { | |
| return (passportDataRects[dataType]!.topLeft - headerThailandRect.topRight) | |
| .distance; | |
| } | |
| TextLine _getTextLineByDistance( | |
| double distanceFromPassport, | |
| double distanceFromThailand, | |
| ) { | |
| // Find the TextLine whose distance to headerPassportRect.topLeft is closest to `distance` | |
| final passportTopLeft = | |
| passportHeader.boundingBox.topLeft; //headerPassportRect.topLeft; | |
| final thailandTopRight = | |
| thailandHeader.boundingBox.topRight; //headerThailandRect.topRight; | |
| final closest = minBy( | |
| lines, | |
| (line) => | |
| ((line.boundingBox.topLeft - passportTopLeft).distance - | |
| distanceFromPassport) | |
| .abs() + | |
| ((line.boundingBox.topLeft - thailandTopRight).distance - | |
| distanceFromThailand) | |
| .abs(), | |
| ); | |
| if (closest == null) { | |
| throw const AppException(message: 'ไม่พบข้อความที่ตรงกับระยะทางที่กำหนด'); | |
| } | |
| return closest; | |
| } | |
| String _getFieldByType( | |
| PassportDataType dataType, | |
| String errorMsg, | |
| ) { | |
| final distanceFromPassport = | |
| _getDistanceFromPassportHeader(dataType) * distanceRatio; | |
| final distanceFromThailand = | |
| _getDistanceFromThailandHeader(dataType) * distanceRatio; | |
| final line = | |
| _getTextLineByDistance(distanceFromPassport, distanceFromThailand); | |
| final trimmed = line.text.trim(); | |
| if (trimmed.isEmpty) { | |
| throw AppException(message: errorMsg); | |
| } | |
| return trimmed; | |
| } | |
| String getSurname() => _getFieldByType( | |
| PassportDataType.surname, | |
| 'ไม่พบ นามสกุล ในรูปภาพพาสปอร์ต', | |
| ); | |
| String getTitleAndName() => _getFieldByType( | |
| PassportDataType.titleAndName, | |
| 'ไม่พบ คำนำหน้าและชื่อ ในรูปภาพพาสปอร์ต', | |
| ); | |
| String getPassportNumber() => _getFieldByType( | |
| PassportDataType.passportNumber, | |
| 'ไม่พบ หมายเลขพาสปอร์ต ในรูปภาพพาสปอร์ต', | |
| ); | |
| String getDateOfBirth() => _getFieldByType( | |
| PassportDataType.dateOfBirth, | |
| 'ไม่พบ วันเกิด ในรูปภาพพาสปอร์ต', | |
| ); | |
| String getIdNumber() => _getFieldByType( | |
| PassportDataType.idNumber, | |
| 'ไม่พบ หมายเลขบัตรประชาชน ในรูปภาพพาสปอร์ต', | |
| ); | |
| String getNationality() => _getFieldByType( | |
| PassportDataType.nationality, | |
| 'ไม่พบ สัญชาติ ในรูปภาพพาสปอร์ต', | |
| ); | |
| String getDateOfExpiry() => _getFieldByType( | |
| PassportDataType.dateOfExpiry, | |
| 'ไม่พบ วันหมดอายุ ในรูปภาพพาสปอร์ต', | |
| ); | |
| } | |
| class AppImageUtils { | |
| static Future<XFile?> showImagePickerBottomSheet(BuildContext context) async { | |
| headerPassportRect.topLeft; | |
| final textTheme = Theme.of(context).textTheme; | |
| final ImageSource? source = await showModalBottomSheet<ImageSource>( | |
| context: context, | |
| shape: const RoundedRectangleBorder( | |
| borderRadius: BorderRadius.vertical( | |
| top: Radius.circular(UiConstants.cardBorderRadius)), | |
| ), | |
| builder: (context) => SafeArea( | |
| child: Column( | |
| mainAxisSize: MainAxisSize.min, | |
| children: [ | |
| Padding( | |
| padding: const EdgeInsets.symmetric(vertical: 16.0), | |
| child: Row( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| Text( | |
| 'ถ่ายรูปหรือเลือกรูป พาสปอร์ต ของคุณ', | |
| style: Theme.of(context).textTheme.titleMedium?.copyWith( | |
| color: Colors.black87, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| Divider(height: 1, thickness: 1, color: Colors.grey.shade300), | |
| ListTile( | |
| leading: const Icon(LucideIcons.camera, | |
| size: 20, color: kIsWeb ? Colors.grey : null), | |
| title: Row( | |
| crossAxisAlignment: CrossAxisAlignment.baseline, | |
| textBaseline: TextBaseline.alphabetic, | |
| children: [ | |
| const Text('ถ่ายรูป', | |
| style: TextStyle(color: kIsWeb ? Colors.grey : null)), | |
| if (kIsWeb) const SizedBox(width: 8), | |
| if (kIsWeb) | |
| Text( | |
| '(ไม่สามารถใช้งานได้บนเว็บ)', | |
| style: textTheme.bodySmall | |
| ?.copyWith(color: Colors.red.shade400), | |
| ), | |
| ], | |
| ), | |
| onTap: kIsWeb | |
| ? null | |
| : () => Navigator.pop(context, ImageSource.camera), | |
| ), | |
| ListTile( | |
| leading: const Icon(LucideIcons.image, size: 20), | |
| title: const Text('เลือกจากคลังภาพ'), | |
| onTap: () => Navigator.pop(context, ImageSource.gallery), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| if (source == null) return null; | |
| return await ImagePicker().pickImage(source: source); | |
| } | |
| // Enhance image: increase contrast and binarize | |
| static Future<XFile> enhanceImage(XFile original) async { | |
| final bytes = await original.readAsBytes(); | |
| // Use compute to run in a background isolate | |
| final enhancedBytes = await compute(_enhanceImageBytes, bytes); | |
| // Get the cache directory | |
| final dir = await getTemporaryDirectory(); | |
| // Use a new file name | |
| final fileName = | |
| '${p.basenameWithoutExtension(original.path)}_enhanced.jpg'; | |
| final tempPath = p.join(dir.path, fileName); | |
| final file = await File(tempPath).writeAsBytes(enhancedBytes); | |
| return XFile(file.path); | |
| } | |
| static Future<String?> extractTextFromImage(XFile image) async { | |
| final inputImage = InputImage.fromFilePath(image.path); | |
| final textRecognizer = TextRecognizer(script: TextRecognitionScript.latin); | |
| try { | |
| final RecognizedText recognizedText = | |
| await textRecognizer.processImage(inputImage); | |
| String msg = ''; | |
| for (TextBlock block in recognizedText.blocks) { | |
| msg += '🟢 Block: ${block.text}\n'; | |
| // final Rect rect = block.boundingBox; | |
| // msg += | |
| // '- Rect: ${rect.left}, ${rect.top}, ${rect.right}, ${rect.bottom}\n'; | |
| // final List<Point<int>> cornerPoints = block.cornerPoints; | |
| // msg += | |
| // '- Corner Points: ${cornerPoints.map((p) => '(${p.x}, ${p.y})').join(', ')}\n'; | |
| // final List<String> languages = block.recognizedLanguages; | |
| // msg += '- Languages: ${languages.join(', ')}\n'; | |
| for (TextLine line in block.lines) { | |
| msg += ' 🟡 Line: ${line.text} - ${line.boundingBox}\n'; | |
| for (TextElement element in line.elements) { | |
| msg += ' 🟠 Element: ${element.text} - ${element.boundingBox}\n'; | |
| } | |
| } | |
| } | |
| debugPrint('Extracted Text: $msg'); | |
| return recognizedText.text; | |
| } catch (e) { | |
| debugPrint('Error extracting text: $e'); | |
| return null; | |
| } finally { | |
| textRecognizer.close(); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment