@ramathibodi/nuxt-commons 0.1.73 → 0.1.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -96
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -0
- package/dist/runtime/components/Alert.vue +58 -54
- package/dist/runtime/components/BarcodeReader.vue +130 -122
- package/dist/runtime/components/ExportCSV.vue +110 -102
- package/dist/runtime/components/FileBtn.vue +79 -67
- package/dist/runtime/components/ImportCSV.vue +151 -139
- package/dist/runtime/components/MrzReader.vue +168 -0
- package/dist/runtime/components/SplitterPanel.vue +67 -59
- package/dist/runtime/components/TabsGroup.vue +39 -31
- package/dist/runtime/components/TextBarcode.vue +66 -54
- package/dist/runtime/components/device/IdCardButton.vue +95 -83
- package/dist/runtime/components/device/IdCardWebSocket.vue +207 -195
- package/dist/runtime/components/device/Scanner.vue +350 -338
- package/dist/runtime/components/dialog/Confirm.vue +112 -100
- package/dist/runtime/components/dialog/Host.vue +88 -84
- package/dist/runtime/components/dialog/Index.vue +84 -72
- package/dist/runtime/components/dialog/Loading.vue +51 -39
- package/dist/runtime/components/dialog/default/Confirm.vue +112 -100
- package/dist/runtime/components/dialog/default/Loading.vue +60 -48
- package/dist/runtime/components/dialog/default/Notify.vue +82 -70
- package/dist/runtime/components/dialog/default/Printing.vue +46 -34
- package/dist/runtime/components/dialog/default/VerifyUser.vue +144 -132
- package/dist/runtime/components/document/Form.vue +50 -42
- package/dist/runtime/components/document/TemplateBuilder.vue +536 -524
- package/dist/runtime/components/form/ActionPad.vue +156 -144
- package/dist/runtime/components/form/Birthdate.vue +116 -104
- package/dist/runtime/components/form/CheckboxGroup.vue +99 -87
- package/dist/runtime/components/form/CodeEditor.vue +45 -37
- package/dist/runtime/components/form/Date.vue +270 -258
- package/dist/runtime/components/form/DateTime.vue +220 -208
- package/dist/runtime/components/form/Dialog.vue +178 -166
- package/dist/runtime/components/form/EditPad.vue +157 -145
- package/dist/runtime/components/form/File.vue +295 -283
- package/dist/runtime/components/form/Hidden.vue +44 -32
- package/dist/runtime/components/form/Iterator.vue +538 -526
- package/dist/runtime/components/form/Login.vue +143 -131
- package/dist/runtime/components/form/Pad.vue +399 -387
- package/dist/runtime/components/form/SignPad.vue +226 -218
- package/dist/runtime/components/form/System.vue +34 -26
- package/dist/runtime/components/form/Table.vue +391 -379
- package/dist/runtime/components/form/TableData.vue +236 -224
- package/dist/runtime/components/form/Time.vue +177 -165
- package/dist/runtime/components/form/images/Capture.vue +245 -237
- package/dist/runtime/components/form/images/Edit.vue +133 -121
- package/dist/runtime/components/form/images/Field.vue +331 -320
- package/dist/runtime/components/form/images/Pad.vue +54 -42
- package/dist/runtime/components/label/Date.vue +37 -29
- package/dist/runtime/components/label/DateAgo.vue +102 -94
- package/dist/runtime/components/label/DateCount.vue +152 -144
- package/dist/runtime/components/label/Field.vue +111 -103
- package/dist/runtime/components/label/FormatMoney.vue +37 -29
- package/dist/runtime/components/label/Mask.vue +46 -38
- package/dist/runtime/components/label/Object.vue +21 -13
- package/dist/runtime/components/master/Autocomplete.vue +89 -81
- package/dist/runtime/components/master/Combobox.vue +88 -80
- package/dist/runtime/components/master/RadioGroup.vue +90 -78
- package/dist/runtime/components/master/Select.vue +70 -62
- package/dist/runtime/components/master/label.vue +55 -47
- package/dist/runtime/components/model/Autocomplete.vue +91 -79
- package/dist/runtime/components/model/Combobox.vue +90 -78
- package/dist/runtime/components/model/Pad.vue +114 -102
- package/dist/runtime/components/model/Select.vue +78 -72
- package/dist/runtime/components/model/Table.vue +370 -358
- package/dist/runtime/components/model/iterator.vue +497 -489
- package/dist/runtime/components/model/label.vue +58 -50
- package/dist/runtime/components/pdf/Print.vue +75 -63
- package/dist/runtime/components/pdf/View.vue +146 -134
- package/dist/runtime/composables/alert.d.ts +4 -0
- package/dist/runtime/composables/api.d.ts +4 -0
- package/dist/runtime/composables/dialog.d.ts +1 -1
- package/dist/runtime/composables/document/templateFormHidden.d.ts +4 -0
- package/dist/runtime/composables/graphql.d.ts +1 -1
- package/dist/runtime/composables/graphqlModel.d.ts +9 -9
- package/dist/runtime/composables/graphqlModelItem.d.ts +7 -7
- package/dist/runtime/composables/graphqlModelOperation.d.ts +6 -6
- package/dist/runtime/composables/localStorageModel.d.ts +4 -0
- package/dist/runtime/composables/lookupList.d.ts +4 -0
- package/dist/runtime/composables/menu.d.ts +4 -0
- package/dist/runtime/composables/useMrzReader.d.ts +48 -0
- package/dist/runtime/composables/useMrzReader.js +423 -0
- package/dist/runtime/composables/useTesseract.d.ts +16 -0
- package/dist/runtime/composables/useTesseract.js +45 -0
- package/dist/runtime/composables/userPermission.d.ts +1 -1
- package/dist/runtime/labs/Calendar.vue +99 -99
- package/dist/runtime/labs/form/EditMobile.vue +152 -152
- package/dist/runtime/labs/form/TextFieldMask.vue +43 -43
- package/dist/runtime/plugins/clientConfig.d.ts +1 -1
- package/dist/runtime/plugins/default.d.ts +1 -1
- package/dist/runtime/plugins/dialogManager.d.ts +1 -1
- package/dist/runtime/plugins/permission.d.ts +1 -1
- package/dist/runtime/types/alert.d.ts +11 -11
- package/dist/runtime/types/clientConfig.d.ts +13 -13
- package/dist/runtime/types/dialogManager.d.ts +35 -35
- package/dist/runtime/types/formDialog.d.ts +5 -5
- package/dist/runtime/types/graphqlOperation.d.ts +23 -23
- package/dist/runtime/types/menu.d.ts +31 -31
- package/dist/runtime/types/modules.d.ts +7 -7
- package/dist/runtime/types/permission.d.ts +13 -13
- package/dist/runtime/utils/asset.d.ts +2 -0
- package/dist/runtime/utils/asset.js +49 -0
- package/package.json +131 -122
- package/scripts/enrich-vue-docs-from-ai.mjs +197 -0
- package/scripts/generate-ai-summary.mjs +321 -0
- package/scripts/generate-composables-md.mjs +129 -0
- package/scripts/postInstall.cjs +70 -70
- package/templates/.codegen/codegen.ts +32 -32
- package/templates/.codegen/plugin-schema-object.js +161 -161
- package/templates/public/tesseract/mrz.traineddata.gz +0 -0
- package/templates/public/tesseract/ocrb.traineddata.gz +0 -0
|
@@ -12,10 +12,10 @@ export interface GraphqlModelConfigProps {
|
|
|
12
12
|
fields?: Array<string | object>;
|
|
13
13
|
}
|
|
14
14
|
export declare function useGraphqlModelOperation<T extends GraphqlModelConfigProps>(props: T): {
|
|
15
|
-
operationCreate: import("vue").ComputedRef<import("~/.nuxt/types/graphqlOperation").graphqlOperationObject<any, any
|
|
16
|
-
operationUpdate: import("vue").ComputedRef<import("~/.nuxt/types/graphqlOperation").graphqlOperationObject<any, any
|
|
17
|
-
operationDelete: import("vue").ComputedRef<import("~/.nuxt/types/graphqlOperation").graphqlOperationObject<any, any
|
|
18
|
-
operationRead: import("vue").ComputedRef<import("~/.nuxt/types/graphqlOperation").graphqlOperationObject<any, any
|
|
19
|
-
operationReadPageable: import("vue").ComputedRef<import("~/.nuxt/types/graphqlOperation").graphqlOperationObject<any, any
|
|
20
|
-
operationSearch: import("vue").ComputedRef<import("~/.nuxt/types/graphqlOperation").graphqlOperationObject<any, any
|
|
15
|
+
operationCreate: import("vue").ComputedRef<import("~/.nuxt/types/graphqlOperation").graphqlOperationObject<any, any> | undefined>;
|
|
16
|
+
operationUpdate: import("vue").ComputedRef<import("~/.nuxt/types/graphqlOperation").graphqlOperationObject<any, any> | undefined>;
|
|
17
|
+
operationDelete: import("vue").ComputedRef<import("~/.nuxt/types/graphqlOperation").graphqlOperationObject<any, any> | undefined>;
|
|
18
|
+
operationRead: import("vue").ComputedRef<import("~/.nuxt/types/graphqlOperation").graphqlOperationObject<any, any> | undefined>;
|
|
19
|
+
operationReadPageable: import("vue").ComputedRef<import("~/.nuxt/types/graphqlOperation").graphqlOperationObject<any, any> | undefined>;
|
|
20
|
+
operationSearch: import("vue").ComputedRef<import("~/.nuxt/types/graphqlOperation").graphqlOperationObject<any, any> | undefined>;
|
|
21
21
|
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLocalStorageModel persists model state in local storage with optional encryption and cache invalidation controls.
|
|
3
|
+
* This doc block is consumed by vue-docgen for generated API documentation.
|
|
4
|
+
*/
|
|
1
5
|
import { type Ref } from 'vue';
|
|
2
6
|
export interface PersistSlimProps {
|
|
3
7
|
/** enable persistence */
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLookupList loads and normalizes lookup options for fields that depend on remote or cached reference lists.
|
|
3
|
+
* This doc block is consumed by vue-docgen for generated API documentation.
|
|
4
|
+
*/
|
|
1
5
|
import { type Ref } from 'vue';
|
|
2
6
|
export interface StaticLookupProps {
|
|
3
7
|
fuzzy?: boolean;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useMenu manages menu configuration, filtering, and visibility behavior for navigation and action surfaces.
|
|
3
|
+
* This doc block is consumed by vue-docgen for generated API documentation.
|
|
4
|
+
*/
|
|
1
5
|
import { type ComputedRef, type InjectionKey, type Ref } from 'vue';
|
|
2
6
|
import { type RouteRecordNormalized, type RouteRecordRaw } from 'vue-router';
|
|
3
7
|
import { type MenuItem } from '../types/menu.js';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check-digit map extracted from a parsed TD3 MRZ payload.
|
|
3
|
+
*/
|
|
4
|
+
export type MrzCheckDigits = {
|
|
5
|
+
passportNumber?: string;
|
|
6
|
+
birthDate?: string;
|
|
7
|
+
expirationDate?: string;
|
|
8
|
+
personalNumber?: string;
|
|
9
|
+
composite?: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Normalized MRZ parsing result after checksum/date validation.
|
|
13
|
+
*/
|
|
14
|
+
export interface MrzResult {
|
|
15
|
+
raw: string;
|
|
16
|
+
lines: string[];
|
|
17
|
+
fields: {
|
|
18
|
+
documentType?: string;
|
|
19
|
+
issuingState?: string;
|
|
20
|
+
surname?: string;
|
|
21
|
+
givenNames?: string;
|
|
22
|
+
passportNumber?: string;
|
|
23
|
+
nationality?: string;
|
|
24
|
+
birthDate?: string;
|
|
25
|
+
sex?: string;
|
|
26
|
+
expirationDate?: string;
|
|
27
|
+
personalNumber?: string;
|
|
28
|
+
checkDigits?: MrzCheckDigits;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export interface UseMrzReaderOptions {
|
|
32
|
+
scaleFactor?: number;
|
|
33
|
+
useOpenCv?: boolean;
|
|
34
|
+
lang?: string;
|
|
35
|
+
langPath?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* MRZ reader pipeline for camera/video/image sources.
|
|
39
|
+
* Performs OCR, candidate-region extraction, TD3 parsing, and checksum validation.
|
|
40
|
+
*/
|
|
41
|
+
export declare function useMrzReader(options?: UseMrzReaderOptions): {
|
|
42
|
+
ensureOpenCvReady: () => Promise<boolean>;
|
|
43
|
+
opencvReady: import("vue").Ref<boolean, boolean>;
|
|
44
|
+
ocrProgress: import("vue").Ref<number, number>;
|
|
45
|
+
ocrStatus: import("vue").Ref<string, string>;
|
|
46
|
+
decodeFromVideoElement: (video: HTMLVideoElement) => Promise<MrzResult | undefined>;
|
|
47
|
+
decodeFromImageFile: (file: File) => Promise<MrzResult | undefined>;
|
|
48
|
+
};
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
import { useTesseract } from "./useTesseract.js";
|
|
3
|
+
export function useMrzReader(options = {}) {
|
|
4
|
+
const scaleFactor = options.scaleFactor ?? 2;
|
|
5
|
+
const useOpenCv = options.useOpenCv ?? true;
|
|
6
|
+
const ocr = useTesseract({
|
|
7
|
+
lang: options.lang ?? "ocrb",
|
|
8
|
+
langPath: options.langPath ?? "/tesseract/"
|
|
9
|
+
});
|
|
10
|
+
const opencvReady = ref(false);
|
|
11
|
+
const opencvTried = ref(false);
|
|
12
|
+
let opencvInstance = null;
|
|
13
|
+
const MRZ_CHARS = /^[A-Z0-9<]+$/;
|
|
14
|
+
const WEIGHTS = [7, 3, 1];
|
|
15
|
+
async function ensureOpenCvReady() {
|
|
16
|
+
if (!useOpenCv || !import.meta.client) return false;
|
|
17
|
+
if (opencvReady.value && opencvInstance) return true;
|
|
18
|
+
if (opencvTried.value) return false;
|
|
19
|
+
opencvTried.value = true;
|
|
20
|
+
const resolveCv = async (mod) => {
|
|
21
|
+
let defaultExport = mod.default;
|
|
22
|
+
if (defaultExport && typeof defaultExport.then === "function") {
|
|
23
|
+
try {
|
|
24
|
+
defaultExport = await defaultExport;
|
|
25
|
+
} catch {
|
|
26
|
+
defaultExport = void 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const first = defaultExport;
|
|
30
|
+
if (first && typeof first.Mat === "function")
|
|
31
|
+
return first;
|
|
32
|
+
const second = mod.cv;
|
|
33
|
+
if (second && typeof second.Mat === "function")
|
|
34
|
+
return second;
|
|
35
|
+
return void 0;
|
|
36
|
+
};
|
|
37
|
+
let importedCv;
|
|
38
|
+
try {
|
|
39
|
+
const mod = await import("@techstark/opencv-js");
|
|
40
|
+
importedCv = await resolveCv(mod);
|
|
41
|
+
} catch {
|
|
42
|
+
importedCv = void 0;
|
|
43
|
+
}
|
|
44
|
+
opencvInstance = importedCv;
|
|
45
|
+
opencvReady.value = !!opencvInstance;
|
|
46
|
+
return opencvReady.value;
|
|
47
|
+
}
|
|
48
|
+
function charValue(ch) {
|
|
49
|
+
if (ch === "<") return 0;
|
|
50
|
+
const code = ch.charCodeAt(0);
|
|
51
|
+
if (code >= 48 && code <= 57) return code - 48;
|
|
52
|
+
if (code >= 65 && code <= 90) return code - 55;
|
|
53
|
+
return -1;
|
|
54
|
+
}
|
|
55
|
+
function checksum(input) {
|
|
56
|
+
let sum = 0;
|
|
57
|
+
for (let i = 0; i < input.length; i++) {
|
|
58
|
+
const value = charValue(input[i]);
|
|
59
|
+
if (value < 0) return void 0;
|
|
60
|
+
sum += value * WEIGHTS[i % 3];
|
|
61
|
+
}
|
|
62
|
+
return String(sum % 10);
|
|
63
|
+
}
|
|
64
|
+
function normalizeMrzText(raw) {
|
|
65
|
+
return raw.toUpperCase().replace(/[^A-Z0-9<\r\n]/g, "").split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
66
|
+
}
|
|
67
|
+
function normalizeLine44(line) {
|
|
68
|
+
return line.replace(/[^A-Z0-9<]/g, "").padEnd(44, "<").slice(0, 44);
|
|
69
|
+
}
|
|
70
|
+
function asDigits(text) {
|
|
71
|
+
return text.replace(/[ODQ]/g, "0").replace(/[IL]/g, "1").replace(/Z/g, "2").replace(/S/g, "5").replace(/B/g, "8").replace(/G/g, "6");
|
|
72
|
+
}
|
|
73
|
+
function asLetters(text) {
|
|
74
|
+
return text.replace(/0/g, "O").replace(/1/g, "I").replace(/2/g, "Z").replace(/5/g, "S").replace(/8/g, "B");
|
|
75
|
+
}
|
|
76
|
+
function normalizeCountryCode(code) {
|
|
77
|
+
const clean = code.replace(/</g, "").trim();
|
|
78
|
+
if (!/^[A-Z]{3}$/.test(clean)) return void 0;
|
|
79
|
+
return clean;
|
|
80
|
+
}
|
|
81
|
+
function isValidDateYYMMDD(value) {
|
|
82
|
+
if (!/^\d{6}$/.test(value)) return false;
|
|
83
|
+
const mm = Number(value.slice(2, 4));
|
|
84
|
+
const dd = Number(value.slice(4, 6));
|
|
85
|
+
return mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31;
|
|
86
|
+
}
|
|
87
|
+
function parseNameField(nameField) {
|
|
88
|
+
const clean = nameField.replace(/<+$/g, "");
|
|
89
|
+
const [rawSurname, rawGiven] = clean.split("<<");
|
|
90
|
+
return {
|
|
91
|
+
surname: rawSurname?.replace(/</g, " ").trim() || void 0,
|
|
92
|
+
givenNames: rawGiven?.replace(/</g, " ").trim() || void 0
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function validateTd3Passport(lines, raw) {
|
|
96
|
+
if (lines.length !== 2) return void 0;
|
|
97
|
+
const line1 = normalizeLine44(lines[0]);
|
|
98
|
+
const line2 = normalizeLine44(lines[1]);
|
|
99
|
+
if (!MRZ_CHARS.test(line1) || !MRZ_CHARS.test(line2)) return void 0;
|
|
100
|
+
const documentType = line1.slice(0, 2).replace(/</g, "").replace(/^F/, "P");
|
|
101
|
+
const issuingState = normalizeCountryCode(asLetters(line1.slice(2, 5)));
|
|
102
|
+
const { surname, givenNames } = parseNameField(line1.slice(5, 44));
|
|
103
|
+
const passportNumber = line2.slice(0, 9);
|
|
104
|
+
const passportCheck = asDigits(line2.slice(9, 10));
|
|
105
|
+
const nationality = normalizeCountryCode(asLetters(line2.slice(10, 13)));
|
|
106
|
+
const birthDate = asDigits(line2.slice(13, 19));
|
|
107
|
+
const birthCheck = asDigits(line2.slice(19, 20));
|
|
108
|
+
const sex = line2.slice(20, 21).replace(/</g, "X");
|
|
109
|
+
const expirationDate = asDigits(line2.slice(21, 27));
|
|
110
|
+
const expirationCheck = asDigits(line2.slice(27, 28));
|
|
111
|
+
const personalNumber = line2.slice(28, 42);
|
|
112
|
+
const personalCheck = asDigits(line2.slice(42, 43));
|
|
113
|
+
const compositeCheck = asDigits(line2.slice(43, 44));
|
|
114
|
+
if (!documentType.startsWith("P")) return void 0;
|
|
115
|
+
if (!/^[A-Z0-9<]{9}$/.test(passportNumber)) return void 0;
|
|
116
|
+
if (!/^[A-Z0-9<]{14}$/.test(personalNumber)) return void 0;
|
|
117
|
+
if (!/^\d$/.test(passportCheck) || !/^\d$/.test(birthCheck) || !/^\d$/.test(expirationCheck) || !/^\d$/.test(personalCheck) || !/^\d$/.test(compositeCheck)) return void 0;
|
|
118
|
+
if (!isValidDateYYMMDD(birthDate) || !isValidDateYYMMDD(expirationDate)) return void 0;
|
|
119
|
+
const numberCalc = checksum(passportNumber);
|
|
120
|
+
const birthCalc = checksum(birthDate);
|
|
121
|
+
const expirationCalc = checksum(expirationDate);
|
|
122
|
+
const personalCalc = checksum(personalNumber);
|
|
123
|
+
const compositeCalc = checksum(`${passportNumber}${passportCheck}${birthDate}${birthCheck}${expirationDate}${expirationCheck}${personalNumber}${personalCheck}`);
|
|
124
|
+
if (!numberCalc || !birthCalc || !expirationCalc || !personalCalc || !compositeCalc) return void 0;
|
|
125
|
+
if (numberCalc !== passportCheck) return void 0;
|
|
126
|
+
if (birthCalc !== birthCheck) return void 0;
|
|
127
|
+
if (expirationCalc !== expirationCheck) return void 0;
|
|
128
|
+
if (personalCalc !== personalCheck) return void 0;
|
|
129
|
+
if (compositeCalc !== compositeCheck) return void 0;
|
|
130
|
+
return {
|
|
131
|
+
raw,
|
|
132
|
+
lines: [line1, line2],
|
|
133
|
+
fields: {
|
|
134
|
+
documentType,
|
|
135
|
+
issuingState,
|
|
136
|
+
surname,
|
|
137
|
+
givenNames,
|
|
138
|
+
passportNumber: passportNumber.replace(/<+$/g, ""),
|
|
139
|
+
nationality,
|
|
140
|
+
birthDate,
|
|
141
|
+
sex,
|
|
142
|
+
expirationDate,
|
|
143
|
+
personalNumber: personalNumber.replace(/<+$/g, ""),
|
|
144
|
+
checkDigits: {
|
|
145
|
+
passportNumber: passportCheck,
|
|
146
|
+
birthDate: birthCheck,
|
|
147
|
+
expirationDate: expirationCheck,
|
|
148
|
+
personalNumber: personalCheck,
|
|
149
|
+
composite: compositeCheck
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function copyFrameToCanvas(source) {
|
|
155
|
+
const canvas = document.createElement("canvas");
|
|
156
|
+
const width = source.videoWidth || source.width;
|
|
157
|
+
const height = source.videoHeight || source.height;
|
|
158
|
+
canvas.width = width;
|
|
159
|
+
canvas.height = height;
|
|
160
|
+
canvas.getContext("2d")?.drawImage(source, 0, 0, width, height);
|
|
161
|
+
return canvas;
|
|
162
|
+
}
|
|
163
|
+
function scoreTwoLineTextBand(grayData, width, height) {
|
|
164
|
+
const totalPixels = width * height;
|
|
165
|
+
if (totalPixels < 2e3) return 0;
|
|
166
|
+
let sum = 0;
|
|
167
|
+
for (let i = 0; i < grayData.length; i += 4)
|
|
168
|
+
sum += grayData[i];
|
|
169
|
+
const mean = sum / totalPixels;
|
|
170
|
+
if (mean < 120) return 0;
|
|
171
|
+
const darkThreshold = Math.max(55, mean - 32);
|
|
172
|
+
const rowCounts = new Float32Array(height);
|
|
173
|
+
for (let y = 0; y < height; y++) {
|
|
174
|
+
let count = 0;
|
|
175
|
+
for (let x = 0; x < width; x++) {
|
|
176
|
+
const idx = (y * width + x) * 4;
|
|
177
|
+
if (grayData[idx] < darkThreshold)
|
|
178
|
+
count++;
|
|
179
|
+
}
|
|
180
|
+
rowCounts[y] = count;
|
|
181
|
+
}
|
|
182
|
+
const smooth = new Float32Array(height);
|
|
183
|
+
for (let y = 0; y < height; y++) {
|
|
184
|
+
const a = rowCounts[Math.max(0, y - 1)];
|
|
185
|
+
const b = rowCounts[y];
|
|
186
|
+
const c = rowCounts[Math.min(height - 1, y + 1)];
|
|
187
|
+
smooth[y] = (a + b + c) / 3;
|
|
188
|
+
}
|
|
189
|
+
let peak1 = 0;
|
|
190
|
+
let peak1Idx = -1;
|
|
191
|
+
for (let i = 0; i < height; i++) {
|
|
192
|
+
if (smooth[i] > peak1) {
|
|
193
|
+
peak1 = smooth[i];
|
|
194
|
+
peak1Idx = i;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
let peak2 = 0;
|
|
198
|
+
for (let i = 0; i < height; i++) {
|
|
199
|
+
if (Math.abs(i - peak1Idx) < Math.max(3, Math.floor(height * 0.14)))
|
|
200
|
+
continue;
|
|
201
|
+
if (smooth[i] > peak2)
|
|
202
|
+
peak2 = smooth[i];
|
|
203
|
+
}
|
|
204
|
+
const avg = smooth.reduce((acc, v) => acc + v, 0) / Math.max(1, smooth.length);
|
|
205
|
+
const prominence = peak1 + peak2 - avg * 2;
|
|
206
|
+
if (peak1 < width * 0.2 || peak2 < width * 0.2) return 0;
|
|
207
|
+
return prominence / Math.max(1, width);
|
|
208
|
+
}
|
|
209
|
+
function findMrzRegionCandidates(sourceCanvas) {
|
|
210
|
+
const srcW = sourceCanvas.width;
|
|
211
|
+
const srcH = sourceCanvas.height;
|
|
212
|
+
if (!srcW || !srcH) return [];
|
|
213
|
+
const probeW = Math.min(640, srcW);
|
|
214
|
+
const scale = probeW / srcW;
|
|
215
|
+
const probeH = Math.max(1, Math.floor(srcH * scale));
|
|
216
|
+
const probe = document.createElement("canvas");
|
|
217
|
+
probe.width = probeW;
|
|
218
|
+
probe.height = probeH;
|
|
219
|
+
const ctx = probe.getContext("2d");
|
|
220
|
+
if (!ctx) return [];
|
|
221
|
+
ctx.drawImage(sourceCanvas, 0, 0, srcW, srcH, 0, 0, probeW, probeH);
|
|
222
|
+
const widths = [0.58, 0.68, 0.78, 0.88, 0.94];
|
|
223
|
+
const heights = [0.14, 0.18, 0.22, 0.26];
|
|
224
|
+
const xCenters = [0.5, 0.42, 0.58, 0.34, 0.66];
|
|
225
|
+
const scored = [];
|
|
226
|
+
for (const wRatio of widths) {
|
|
227
|
+
for (const hRatio of heights) {
|
|
228
|
+
const w = Math.max(56, Math.floor(probeW * wRatio));
|
|
229
|
+
const h = Math.max(30, Math.floor(probeH * hRatio));
|
|
230
|
+
const yStart = Math.floor(probeH * 0.24);
|
|
231
|
+
const yEnd = Math.floor(probeH * 0.94);
|
|
232
|
+
const step = Math.max(10, Math.floor(h * 0.28));
|
|
233
|
+
for (const center of xCenters) {
|
|
234
|
+
const x = Math.max(0, Math.min(probeW - w, Math.floor(probeW * center - w / 2)));
|
|
235
|
+
for (let y = yStart; y + h < yEnd; y += step) {
|
|
236
|
+
const img = ctx.getImageData(x, y, w, h);
|
|
237
|
+
const score = scoreTwoLineTextBand(img.data, w, h);
|
|
238
|
+
if (score <= 0) continue;
|
|
239
|
+
scored.push({ x: Math.round(x / scale), y: Math.round(y / scale), w: Math.round(w / scale), h: Math.round(h / scale), score });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
scored.sort((a, b) => b.score - a.score);
|
|
245
|
+
return scored.slice(0, 4);
|
|
246
|
+
}
|
|
247
|
+
function findMrzCandidatesWithOpenCv(sourceCanvas) {
|
|
248
|
+
if (!opencvReady.value || !opencvInstance) return [];
|
|
249
|
+
const cv = opencvInstance;
|
|
250
|
+
let src;
|
|
251
|
+
let gray;
|
|
252
|
+
let blur;
|
|
253
|
+
let edges;
|
|
254
|
+
let kernel;
|
|
255
|
+
let closed;
|
|
256
|
+
let contours;
|
|
257
|
+
let hierarchy;
|
|
258
|
+
try {
|
|
259
|
+
src = cv.imread(sourceCanvas);
|
|
260
|
+
gray = new cv.Mat();
|
|
261
|
+
blur = new cv.Mat();
|
|
262
|
+
edges = new cv.Mat();
|
|
263
|
+
kernel = cv.getStructuringElement(cv.MORPH_RECT, new cv.Size(9, 3));
|
|
264
|
+
closed = new cv.Mat();
|
|
265
|
+
contours = new cv.MatVector();
|
|
266
|
+
hierarchy = new cv.Mat();
|
|
267
|
+
cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY, 0);
|
|
268
|
+
cv.GaussianBlur(gray, blur, new cv.Size(5, 5), 0, 0, cv.BORDER_DEFAULT);
|
|
269
|
+
cv.Canny(blur, edges, 50, 140, 3, false);
|
|
270
|
+
cv.morphologyEx(edges, closed, cv.MORPH_CLOSE, kernel);
|
|
271
|
+
cv.findContours(closed, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE);
|
|
272
|
+
const output = [];
|
|
273
|
+
for (let i = 0; i < contours.size(); i++) {
|
|
274
|
+
const contour = contours.get(i);
|
|
275
|
+
const rect = cv.boundingRect(contour);
|
|
276
|
+
contour.delete?.();
|
|
277
|
+
const ratio = rect.width / Math.max(1, rect.height);
|
|
278
|
+
if (rect.width < sourceCanvas.width * 0.45 || rect.height < sourceCanvas.height * 0.1) continue;
|
|
279
|
+
if (ratio < 2.2 || ratio > 10) continue;
|
|
280
|
+
output.push({
|
|
281
|
+
x: rect.x,
|
|
282
|
+
y: rect.y,
|
|
283
|
+
w: rect.width,
|
|
284
|
+
h: rect.height,
|
|
285
|
+
score: rect.width * rect.height / (sourceCanvas.width * sourceCanvas.height)
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
output.sort((a, b) => b.score - a.score);
|
|
289
|
+
return output.slice(0, 4);
|
|
290
|
+
} catch {
|
|
291
|
+
return [];
|
|
292
|
+
} finally {
|
|
293
|
+
src?.delete?.();
|
|
294
|
+
gray?.delete?.();
|
|
295
|
+
blur?.delete?.();
|
|
296
|
+
edges?.delete?.();
|
|
297
|
+
kernel?.delete?.();
|
|
298
|
+
closed?.delete?.();
|
|
299
|
+
contours?.delete?.();
|
|
300
|
+
hierarchy?.delete?.();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function applyBinarize(canvas) {
|
|
304
|
+
const ctx = canvas.getContext("2d");
|
|
305
|
+
if (!ctx) return;
|
|
306
|
+
const img = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
307
|
+
const data = img.data;
|
|
308
|
+
let min = 255;
|
|
309
|
+
let max = 0;
|
|
310
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
311
|
+
const g = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
|
312
|
+
if (g < min) min = g;
|
|
313
|
+
if (g > max) max = g;
|
|
314
|
+
}
|
|
315
|
+
const range = Math.max(1, max - min);
|
|
316
|
+
const threshold = 0.58 * 255;
|
|
317
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
318
|
+
const g = (0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2] - min) * (255 / range);
|
|
319
|
+
const v = g > threshold ? 255 : 0;
|
|
320
|
+
data[i] = v;
|
|
321
|
+
data[i + 1] = v;
|
|
322
|
+
data[i + 2] = v;
|
|
323
|
+
data[i + 3] = 255;
|
|
324
|
+
}
|
|
325
|
+
ctx.putImageData(img, 0, 0);
|
|
326
|
+
}
|
|
327
|
+
function renderCandidate(outputCanvas, sourceCanvas, candidate, angleDeg = 0, zoom = 1, binarize = true) {
|
|
328
|
+
outputCanvas.width = Math.max(1, Math.floor(candidate.w * zoom));
|
|
329
|
+
outputCanvas.height = Math.max(1, Math.floor(candidate.h * zoom));
|
|
330
|
+
const ctx = outputCanvas.getContext("2d");
|
|
331
|
+
if (!ctx) return;
|
|
332
|
+
ctx.save();
|
|
333
|
+
ctx.translate(outputCanvas.width / 2, outputCanvas.height / 2);
|
|
334
|
+
ctx.rotate(angleDeg * Math.PI / 180);
|
|
335
|
+
ctx.drawImage(
|
|
336
|
+
sourceCanvas,
|
|
337
|
+
candidate.x,
|
|
338
|
+
candidate.y,
|
|
339
|
+
candidate.w,
|
|
340
|
+
candidate.h,
|
|
341
|
+
-outputCanvas.width / 2,
|
|
342
|
+
-outputCanvas.height / 2,
|
|
343
|
+
outputCanvas.width,
|
|
344
|
+
outputCanvas.height
|
|
345
|
+
);
|
|
346
|
+
ctx.restore();
|
|
347
|
+
if (binarize)
|
|
348
|
+
applyBinarize(outputCanvas);
|
|
349
|
+
}
|
|
350
|
+
function pickPassportCandidates(lines) {
|
|
351
|
+
const candidates = [];
|
|
352
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
353
|
+
if (lines[i].length < 30 || lines[i + 1].length < 30) continue;
|
|
354
|
+
candidates.push([normalizeLine44(lines[i]), normalizeLine44(lines[i + 1])]);
|
|
355
|
+
}
|
|
356
|
+
if (!candidates.length) {
|
|
357
|
+
const sorted = [...lines].sort((a, b) => b.length - a.length);
|
|
358
|
+
if (sorted.length >= 2)
|
|
359
|
+
candidates.push([normalizeLine44(sorted[0]), normalizeLine44(sorted[1])]);
|
|
360
|
+
}
|
|
361
|
+
return candidates;
|
|
362
|
+
}
|
|
363
|
+
async function decodeFromCanvas(sourceCanvas) {
|
|
364
|
+
await ocr.ensureReady();
|
|
365
|
+
if (useOpenCv)
|
|
366
|
+
await ensureOpenCvReady();
|
|
367
|
+
const candidates = opencvReady.value ? findMrzCandidatesWithOpenCv(sourceCanvas) : findMrzRegionCandidates(sourceCanvas);
|
|
368
|
+
const fallback = {
|
|
369
|
+
x: Math.floor(sourceCanvas.width * 0.05),
|
|
370
|
+
y: Math.floor(sourceCanvas.height * 0.64),
|
|
371
|
+
w: Math.floor(sourceCanvas.width * 0.9),
|
|
372
|
+
h: Math.floor(sourceCanvas.height * 0.24),
|
|
373
|
+
score: 0
|
|
374
|
+
};
|
|
375
|
+
const selected = (candidates.length ? candidates : [fallback]).slice(0, 3);
|
|
376
|
+
const probe = document.createElement("canvas");
|
|
377
|
+
for (const candidate of selected) {
|
|
378
|
+
for (const angle of [0, -6, 6]) {
|
|
379
|
+
for (const binarize of [true, false]) {
|
|
380
|
+
renderCandidate(probe, sourceCanvas, candidate, angle, scaleFactor, binarize);
|
|
381
|
+
const raw = await ocr.recognize(probe);
|
|
382
|
+
const lines = normalizeMrzText(raw);
|
|
383
|
+
const mrzCandidates = pickPassportCandidates(lines);
|
|
384
|
+
for (const mrz of mrzCandidates) {
|
|
385
|
+
const parsed = validateTd3Passport(mrz, raw);
|
|
386
|
+
if (parsed) return parsed;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return void 0;
|
|
392
|
+
}
|
|
393
|
+
async function decodeFromVideoElement(video) {
|
|
394
|
+
const canvas = copyFrameToCanvas(video);
|
|
395
|
+
return decodeFromCanvas(canvas);
|
|
396
|
+
}
|
|
397
|
+
async function decodeFromImageFile(file) {
|
|
398
|
+
const image = await new Promise((resolve, reject) => {
|
|
399
|
+
const reader = new FileReader();
|
|
400
|
+
reader.onload = () => {
|
|
401
|
+
const img = new Image();
|
|
402
|
+
img.onload = () => resolve(img);
|
|
403
|
+
img.onerror = () => reject(new Error("Failed to load image"));
|
|
404
|
+
img.src = reader.result;
|
|
405
|
+
};
|
|
406
|
+
reader.onerror = () => reject(new Error("Failed to read file"));
|
|
407
|
+
reader.readAsDataURL(file);
|
|
408
|
+
});
|
|
409
|
+
const canvas = document.createElement("canvas");
|
|
410
|
+
canvas.width = image.naturalWidth;
|
|
411
|
+
canvas.height = image.naturalHeight;
|
|
412
|
+
canvas.getContext("2d")?.drawImage(image, 0, 0);
|
|
413
|
+
return decodeFromCanvas(canvas);
|
|
414
|
+
}
|
|
415
|
+
return {
|
|
416
|
+
ensureOpenCvReady,
|
|
417
|
+
opencvReady,
|
|
418
|
+
ocrProgress: ocr.progress,
|
|
419
|
+
ocrStatus: ocr.status,
|
|
420
|
+
decodeFromVideoElement,
|
|
421
|
+
decodeFromImageFile
|
|
422
|
+
};
|
|
423
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for configuring client-side Tesseract OCR behavior.
|
|
3
|
+
*/
|
|
4
|
+
export interface UseTesseractOptions {
|
|
5
|
+
lang?: string;
|
|
6
|
+
langPath?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Provides lazy-loaded Tesseract OCR helpers with reactive status/progress state.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useTesseract(options?: UseTesseractOptions): {
|
|
12
|
+
progress: import("vue").Ref<number, number>;
|
|
13
|
+
status: import("vue").Ref<string, string>;
|
|
14
|
+
ensureReady: () => Promise<typeof import("tesseract.js")>;
|
|
15
|
+
recognize: (canvas: HTMLCanvasElement) => Promise<string>;
|
|
16
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ref } from "vue";
|
|
2
|
+
export function useTesseract(options = {}) {
|
|
3
|
+
const progress = ref(0);
|
|
4
|
+
const status = ref("idle");
|
|
5
|
+
const lang = options.lang ?? "ocrb";
|
|
6
|
+
const langPath = options.langPath ?? "/tesseract/";
|
|
7
|
+
let lib = null;
|
|
8
|
+
function resolvedLangPath() {
|
|
9
|
+
if (!import.meta.client)
|
|
10
|
+
return langPath;
|
|
11
|
+
return new URL(langPath, window.location.origin).toString();
|
|
12
|
+
}
|
|
13
|
+
async function ensureReady() {
|
|
14
|
+
if (!import.meta.client)
|
|
15
|
+
throw new Error("Tesseract OCR is client-only.");
|
|
16
|
+
if (!lib) {
|
|
17
|
+
lib = await import("tesseract.js");
|
|
18
|
+
}
|
|
19
|
+
return lib;
|
|
20
|
+
}
|
|
21
|
+
async function recognize(canvas) {
|
|
22
|
+
const tesseract = await ensureReady();
|
|
23
|
+
progress.value = 0;
|
|
24
|
+
status.value = "starting";
|
|
25
|
+
const result = await tesseract.recognize(canvas, lang, {
|
|
26
|
+
gzip: true,
|
|
27
|
+
cacheMethod: "refresh",
|
|
28
|
+
langPath: resolvedLangPath(),
|
|
29
|
+
logger: (m) => {
|
|
30
|
+
if (m?.status)
|
|
31
|
+
status.value = m.status;
|
|
32
|
+
if (m?.status === "recognizing text" && typeof m?.progress === "number")
|
|
33
|
+
progress.value = Math.round(m.progress * 100);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
status.value = "idle";
|
|
37
|
+
return result?.data?.text || "";
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
progress,
|
|
41
|
+
status,
|
|
42
|
+
ensureReady,
|
|
43
|
+
recognize
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const useUserPermission: () =>
|
|
1
|
+
export declare const useUserPermission: () => any;
|