Skip to content

Instantly share code, notes, and snippets.

@3bugs
Created June 29, 2025 04:19
Show Gist options
  • Select an option

  • Save 3bugs/8d1810e6dd1d83b1134c14b8e8f41108 to your computer and use it in GitHub Desktop.

Select an option

Save 3bugs/8d1810e6dd1d83b1134c14b8e8f41108 to your computer and use it in GitHub Desktop.
// 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