@mleonard9/vin-scanner 0.1.1
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 +110 -0
- package/android/build.gradle +102 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +8 -0
- package/android/src/main/AndroidManifestNew.xml +2 -0
- package/android/src/main/java/com/visioncamerabarcodescanner/VisionCameraBarcodeScannerModule.kt +93 -0
- package/android/src/main/java/com/visioncamerabarcodescanner/VisionCameraBarcodeScannerPackage.kt +27 -0
- package/android/src/main/java/com/visioncameratextrecognition/VisionCameraTextRecognitionModule.kt +130 -0
- package/ios/VisionCameraBarcodeScanner.m +175 -0
- package/ios/VisionCameraTextRecognition.m +114 -0
- package/lib/commonjs/index.js +87 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/scanBarcodes.js +46 -0
- package/lib/commonjs/scanBarcodes.js.map +1 -0
- package/lib/commonjs/scanText.js +29 -0
- package/lib/commonjs/scanText.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/vinUtils.js +240 -0
- package/lib/commonjs/vinUtils.js.map +1 -0
- package/lib/module/index.js +80 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/scanBarcodes.js +40 -0
- package/lib/module/scanBarcodes.js.map +1 -0
- package/lib/module/scanText.js +23 -0
- package/lib/module/scanText.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/vinUtils.js +230 -0
- package/lib/module/vinUtils.js.map +1 -0
- package/lib/typescript/src/index.d.ts +6 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/scanBarcodes.d.ts +3 -0
- package/lib/typescript/src/scanBarcodes.d.ts.map +1 -0
- package/lib/typescript/src/scanText.d.ts +3 -0
- package/lib/typescript/src/scanText.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +104 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/vinUtils.d.ts +19 -0
- package/lib/typescript/src/vinUtils.d.ts.map +1 -0
- package/package.json +168 -0
- package/src/index.tsx +131 -0
- package/src/scanBarcodes.ts +76 -0
- package/src/scanText.ts +36 -0
- package/src/types.ts +145 -0
- package/src/vinUtils.ts +368 -0
- package/vin-scanner.podspec +28 -0
package/src/vinUtils.ts
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BarcodeDetection,
|
|
3
|
+
DetectionOptions,
|
|
4
|
+
ScanBarcodeOptions,
|
|
5
|
+
TextDetection,
|
|
6
|
+
TextRecognitionLanguage,
|
|
7
|
+
VinCandidate,
|
|
8
|
+
VinResultMode,
|
|
9
|
+
VinScannerOptions,
|
|
10
|
+
WorkletPayload,
|
|
11
|
+
} from './types';
|
|
12
|
+
|
|
13
|
+
export type ResolvedVinScannerOptions = {
|
|
14
|
+
barcode: {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
formats: ScanBarcodeOptions;
|
|
17
|
+
};
|
|
18
|
+
text: {
|
|
19
|
+
enabled: boolean;
|
|
20
|
+
language: TextRecognitionLanguage;
|
|
21
|
+
};
|
|
22
|
+
detection: Required<DetectionOptions> & { resultMode: VinResultMode };
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const DEFAULT_RESOLVED_OPTIONS: ResolvedVinScannerOptions = {
|
|
26
|
+
barcode: {
|
|
27
|
+
enabled: true,
|
|
28
|
+
formats: ['all'],
|
|
29
|
+
},
|
|
30
|
+
text: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
language: 'latin',
|
|
33
|
+
},
|
|
34
|
+
detection: {
|
|
35
|
+
resultMode: 'best',
|
|
36
|
+
preferBarcode: true,
|
|
37
|
+
validateChecksum: true,
|
|
38
|
+
emitDuplicates: false,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const resolveOptions = (
|
|
43
|
+
options?: VinScannerOptions
|
|
44
|
+
): ResolvedVinScannerOptions => {
|
|
45
|
+
const resultMode: VinResultMode =
|
|
46
|
+
options?.detection?.resultMode ??
|
|
47
|
+
DEFAULT_RESOLVED_OPTIONS.detection.resultMode;
|
|
48
|
+
const emitDuplicates =
|
|
49
|
+
options?.detection?.emitDuplicates ??
|
|
50
|
+
(resultMode === 'all'
|
|
51
|
+
? true
|
|
52
|
+
: DEFAULT_RESOLVED_OPTIONS.detection.emitDuplicates);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
barcode: {
|
|
56
|
+
enabled:
|
|
57
|
+
options?.barcode?.enabled ?? DEFAULT_RESOLVED_OPTIONS.barcode.enabled,
|
|
58
|
+
formats: options?.barcode?.formats?.length
|
|
59
|
+
? [...options.barcode.formats]
|
|
60
|
+
: [...DEFAULT_RESOLVED_OPTIONS.barcode.formats],
|
|
61
|
+
},
|
|
62
|
+
text: {
|
|
63
|
+
enabled: options?.text?.enabled ?? DEFAULT_RESOLVED_OPTIONS.text.enabled,
|
|
64
|
+
language:
|
|
65
|
+
options?.text?.language ?? DEFAULT_RESOLVED_OPTIONS.text.language,
|
|
66
|
+
},
|
|
67
|
+
detection: {
|
|
68
|
+
resultMode,
|
|
69
|
+
preferBarcode:
|
|
70
|
+
options?.detection?.preferBarcode ??
|
|
71
|
+
DEFAULT_RESOLVED_OPTIONS.detection.preferBarcode,
|
|
72
|
+
validateChecksum:
|
|
73
|
+
options?.detection?.validateChecksum ??
|
|
74
|
+
DEFAULT_RESOLVED_OPTIONS.detection.validateChecksum,
|
|
75
|
+
emitDuplicates,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const VIN_PATTERN = /^[A-HJ-NPR-Z0-9]{17}$/;
|
|
81
|
+
|
|
82
|
+
const transliteration: Record<string, number> = {
|
|
83
|
+
A: 1,
|
|
84
|
+
B: 2,
|
|
85
|
+
C: 3,
|
|
86
|
+
D: 4,
|
|
87
|
+
E: 5,
|
|
88
|
+
F: 6,
|
|
89
|
+
G: 7,
|
|
90
|
+
H: 8,
|
|
91
|
+
J: 1,
|
|
92
|
+
K: 2,
|
|
93
|
+
L: 3,
|
|
94
|
+
M: 4,
|
|
95
|
+
N: 5,
|
|
96
|
+
P: 7,
|
|
97
|
+
R: 9,
|
|
98
|
+
S: 2,
|
|
99
|
+
T: 3,
|
|
100
|
+
U: 4,
|
|
101
|
+
V: 5,
|
|
102
|
+
W: 6,
|
|
103
|
+
X: 7,
|
|
104
|
+
Y: 8,
|
|
105
|
+
Z: 9,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const weights = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
|
|
109
|
+
|
|
110
|
+
const tokenizeCandidate = (value: string): string[] => {
|
|
111
|
+
if (!value) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
const normalized = value.toUpperCase();
|
|
115
|
+
const cleaned = normalized.replace(/[^A-Z0-9]/g, ' ');
|
|
116
|
+
const tokens = cleaned.split(/\s+/).filter((token) => token.length > 0);
|
|
117
|
+
const matches = new Set<string>();
|
|
118
|
+
tokens.forEach((token) => {
|
|
119
|
+
if (token.length < 17) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
for (let i = 0; i <= token.length - 17; i += 1) {
|
|
123
|
+
const candidate = token.slice(i, i + 17);
|
|
124
|
+
if (VIN_PATTERN.test(candidate)) {
|
|
125
|
+
matches.add(candidate);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return Array.from(matches);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const isValidVin = (value: string): boolean => {
|
|
133
|
+
if (!VIN_PATTERN.test(value)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const chars = value.split('');
|
|
137
|
+
let sum = 0;
|
|
138
|
+
for (let i = 0; i < chars.length; i += 1) {
|
|
139
|
+
const char = chars[i];
|
|
140
|
+
if (!char) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
const numeric = /^[0-9]$/.test(char)
|
|
144
|
+
? Number.parseInt(char, 10)
|
|
145
|
+
: transliteration[char];
|
|
146
|
+
if (numeric === undefined) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const weight = weights[i] ?? 0;
|
|
150
|
+
sum += numeric * weight;
|
|
151
|
+
}
|
|
152
|
+
const remainder = sum % 11;
|
|
153
|
+
const expected = remainder === 10 ? 'X' : remainder.toString();
|
|
154
|
+
const checkDigit = chars[8];
|
|
155
|
+
if (!checkDigit) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return checkDigit === expected;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const toBoundingBox = (
|
|
162
|
+
candidate:
|
|
163
|
+
| Pick<
|
|
164
|
+
BarcodeDetection,
|
|
165
|
+
'top' | 'bottom' | 'left' | 'right' | 'width' | 'height'
|
|
166
|
+
>
|
|
167
|
+
| Pick<
|
|
168
|
+
TextDetection,
|
|
169
|
+
| 'blockFrameTop'
|
|
170
|
+
| 'blockFrameBottom'
|
|
171
|
+
| 'blockFrameLeft'
|
|
172
|
+
| 'blockFrameRight'
|
|
173
|
+
| 'lineFrameTop'
|
|
174
|
+
| 'lineFrameBottom'
|
|
175
|
+
| 'lineFrameLeft'
|
|
176
|
+
| 'lineFrameRight'
|
|
177
|
+
| 'elementFrameTop'
|
|
178
|
+
| 'elementFrameBottom'
|
|
179
|
+
| 'elementFrameLeft'
|
|
180
|
+
| 'elementFrameRight'
|
|
181
|
+
>,
|
|
182
|
+
preferred: 'block' | 'line' | 'element' | 'barcode'
|
|
183
|
+
) => {
|
|
184
|
+
if (!candidate) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
if (preferred === 'barcode') {
|
|
188
|
+
const maybe = candidate as BarcodeDetection;
|
|
189
|
+
if (
|
|
190
|
+
typeof maybe.top === 'number' &&
|
|
191
|
+
typeof maybe.bottom === 'number' &&
|
|
192
|
+
typeof maybe.left === 'number' &&
|
|
193
|
+
typeof maybe.right === 'number'
|
|
194
|
+
) {
|
|
195
|
+
return {
|
|
196
|
+
top: maybe.top,
|
|
197
|
+
bottom: maybe.bottom,
|
|
198
|
+
left: maybe.left,
|
|
199
|
+
right: maybe.right,
|
|
200
|
+
width: maybe.width,
|
|
201
|
+
height: maybe.height,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const block = candidate as TextDetection;
|
|
207
|
+
const blockBox =
|
|
208
|
+
typeof block.blockFrameTop === 'number' &&
|
|
209
|
+
typeof block.blockFrameBottom === 'number' &&
|
|
210
|
+
typeof block.blockFrameLeft === 'number' &&
|
|
211
|
+
typeof block.blockFrameRight === 'number'
|
|
212
|
+
? {
|
|
213
|
+
top: block.blockFrameTop,
|
|
214
|
+
bottom: block.blockFrameBottom,
|
|
215
|
+
left: block.blockFrameLeft,
|
|
216
|
+
right: block.blockFrameRight,
|
|
217
|
+
}
|
|
218
|
+
: undefined;
|
|
219
|
+
const lineBox =
|
|
220
|
+
typeof block.lineFrameTop === 'number' &&
|
|
221
|
+
typeof block.lineFrameBottom === 'number' &&
|
|
222
|
+
typeof block.lineFrameLeft === 'number' &&
|
|
223
|
+
typeof block.lineFrameRight === 'number'
|
|
224
|
+
? {
|
|
225
|
+
top: block.lineFrameTop,
|
|
226
|
+
bottom: block.lineFrameBottom,
|
|
227
|
+
left: block.lineFrameLeft,
|
|
228
|
+
right: block.lineFrameRight,
|
|
229
|
+
}
|
|
230
|
+
: undefined;
|
|
231
|
+
const elementBox =
|
|
232
|
+
typeof block.elementFrameTop === 'number' &&
|
|
233
|
+
typeof block.elementFrameBottom === 'number' &&
|
|
234
|
+
typeof block.elementFrameLeft === 'number' &&
|
|
235
|
+
typeof block.elementFrameRight === 'number'
|
|
236
|
+
? {
|
|
237
|
+
top: block.elementFrameTop,
|
|
238
|
+
bottom: block.elementFrameBottom,
|
|
239
|
+
left: block.elementFrameLeft,
|
|
240
|
+
right: block.elementFrameRight,
|
|
241
|
+
}
|
|
242
|
+
: undefined;
|
|
243
|
+
|
|
244
|
+
if (preferred === 'element' && elementBox) {
|
|
245
|
+
return elementBox;
|
|
246
|
+
}
|
|
247
|
+
if (preferred === 'line' && lineBox) {
|
|
248
|
+
return lineBox;
|
|
249
|
+
}
|
|
250
|
+
return blockBox || lineBox || elementBox;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const candidateFromBarcode = (
|
|
254
|
+
detection: BarcodeDetection,
|
|
255
|
+
validateChecksum: boolean
|
|
256
|
+
): VinCandidate[] => {
|
|
257
|
+
const values = new Set<string>();
|
|
258
|
+
tokenizeCandidate(detection.rawValue ?? '').forEach((value) =>
|
|
259
|
+
values.add(value)
|
|
260
|
+
);
|
|
261
|
+
tokenizeCandidate(detection.displayValue ?? '').forEach((value) =>
|
|
262
|
+
values.add(value)
|
|
263
|
+
);
|
|
264
|
+
const boundingBox = toBoundingBox(detection, 'barcode');
|
|
265
|
+
return Array.from(values).map((value) => {
|
|
266
|
+
const checksumValid = !validateChecksum || isValidVin(value);
|
|
267
|
+
return {
|
|
268
|
+
value,
|
|
269
|
+
source: 'barcode' as const,
|
|
270
|
+
confidence: checksumValid ? 0.95 : 0.8,
|
|
271
|
+
boundingBox,
|
|
272
|
+
origin: detection.displayValue ? 'displayValue' : 'rawValue',
|
|
273
|
+
rawPayload: detection,
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const candidateFromText = (
|
|
279
|
+
detection: TextDetection,
|
|
280
|
+
validateChecksum: boolean
|
|
281
|
+
): VinCandidate[] => {
|
|
282
|
+
const values = new Set<string>();
|
|
283
|
+
[
|
|
284
|
+
detection.lineText,
|
|
285
|
+
detection.elementText,
|
|
286
|
+
detection.blockText,
|
|
287
|
+
detection.resultText,
|
|
288
|
+
].forEach((value) =>
|
|
289
|
+
tokenizeCandidate(value ?? '').forEach((v) => values.add(v))
|
|
290
|
+
);
|
|
291
|
+
const boundingBox =
|
|
292
|
+
toBoundingBox(detection, 'line') ??
|
|
293
|
+
toBoundingBox(detection, 'element') ??
|
|
294
|
+
toBoundingBox(detection, 'block');
|
|
295
|
+
return Array.from(values).map((value) => {
|
|
296
|
+
const checksumValid = !validateChecksum || isValidVin(value);
|
|
297
|
+
return {
|
|
298
|
+
value,
|
|
299
|
+
source: 'text' as const,
|
|
300
|
+
confidence: checksumValid ? 0.8 : 0.6,
|
|
301
|
+
boundingBox,
|
|
302
|
+
origin: detection.elementText
|
|
303
|
+
? 'element'
|
|
304
|
+
: detection.lineText
|
|
305
|
+
? 'line'
|
|
306
|
+
: 'block',
|
|
307
|
+
rawPayload: detection,
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const dedupeCandidates = (candidates: VinCandidate[]): VinCandidate[] => {
|
|
313
|
+
const map = new Map<string, VinCandidate>();
|
|
314
|
+
candidates.forEach((candidate) => {
|
|
315
|
+
const key = [
|
|
316
|
+
candidate.value,
|
|
317
|
+
candidate.source,
|
|
318
|
+
candidate.boundingBox?.top ?? 'x',
|
|
319
|
+
candidate.boundingBox?.left ?? 'y',
|
|
320
|
+
].join(':');
|
|
321
|
+
const existing = map.get(key);
|
|
322
|
+
if (!existing || existing.confidence < candidate.confidence) {
|
|
323
|
+
map.set(key, candidate);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
return Array.from(map.values());
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export const buildVinCandidates = (
|
|
330
|
+
payload: WorkletPayload,
|
|
331
|
+
options: ResolvedVinScannerOptions
|
|
332
|
+
): VinCandidate[] => {
|
|
333
|
+
const list: VinCandidate[] = [];
|
|
334
|
+
if (options.barcode.enabled) {
|
|
335
|
+
(payload.barcodes ?? []).forEach((barcode) => {
|
|
336
|
+
list.push(
|
|
337
|
+
...candidateFromBarcode(barcode, options.detection.validateChecksum)
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (options.text.enabled) {
|
|
342
|
+
(payload.textBlocks ?? []).forEach((textBlock) => {
|
|
343
|
+
list.push(
|
|
344
|
+
...candidateFromText(textBlock, options.detection.validateChecksum)
|
|
345
|
+
);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return dedupeCandidates(list);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
export const pickBestCandidate = (
|
|
352
|
+
candidates: VinCandidate[],
|
|
353
|
+
options: ResolvedVinScannerOptions
|
|
354
|
+
): VinCandidate | null => {
|
|
355
|
+
if (!candidates.length) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const sorted = [...candidates].sort((a, b) => {
|
|
359
|
+
if (a.confidence !== b.confidence) {
|
|
360
|
+
return b.confidence - a.confidence;
|
|
361
|
+
}
|
|
362
|
+
if (options.detection.preferBarcode && a.source !== b.source) {
|
|
363
|
+
return a.source === 'barcode' ? -1 : 1;
|
|
364
|
+
}
|
|
365
|
+
return 0;
|
|
366
|
+
});
|
|
367
|
+
return sorted[0] ?? null;
|
|
368
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
|
|
5
|
+
|
|
6
|
+
Pod::Spec.new do |s|
|
|
7
|
+
s.name = "vin-scanner"
|
|
8
|
+
s.version = package["version"]
|
|
9
|
+
s.summary = package["description"]
|
|
10
|
+
s.homepage = package["homepage"]
|
|
11
|
+
s.license = package["license"]
|
|
12
|
+
s.authors = package["author"]
|
|
13
|
+
|
|
14
|
+
s.platforms = { :ios => min_ios_version_supported }
|
|
15
|
+
s.source = { :git => "https://github.com/mleonard9/vin-scanner.git", :tag => "#{s.version}" }
|
|
16
|
+
|
|
17
|
+
s.source_files = "ios/**/*.{h,m,mm}"
|
|
18
|
+
|
|
19
|
+
s.dependency "React-Core"
|
|
20
|
+
s.dependency "VisionCamera"
|
|
21
|
+
s.dependency "GoogleMLKit/BarcodeScanning"
|
|
22
|
+
s.dependency "GoogleMLKit/TextRecognition"
|
|
23
|
+
s.dependency "GoogleMLKit/TextRecognitionChinese"
|
|
24
|
+
s.dependency "GoogleMLKit/TextRecognitionDevanagari"
|
|
25
|
+
s.dependency "GoogleMLKit/TextRecognitionJapanese"
|
|
26
|
+
s.dependency "GoogleMLKit/TextRecognitionKorean"
|
|
27
|
+
|
|
28
|
+
end
|