capacitor-card-scanner 0.0.1 → 0.2.0
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 +136 -3
- package/android/build.gradle +2 -0
- package/android/src/main/java/pt/tcgclassifier/cardscanner/CardDetector.java +283 -0
- package/android/src/main/java/pt/tcgclassifier/cardscanner/CardScanner.java +267 -195
- package/android/src/main/java/pt/tcgclassifier/cardscanner/CardScannerPlugin.java +33 -0
- package/android/src/main/java/pt/tcgclassifier/cardscanner/DetectionOverlay.java +82 -0
- package/dist/docs.json +300 -0
- package/dist/esm/definitions.d.ts +83 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +2 -0
- package/dist/esm/web.js +6 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +6 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +6 -0
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,13 +45,16 @@ Add to the app's `Info.plist`:
|
|
|
45
45
|
1. Add a CSS class to the Theme / screen:
|
|
46
46
|
|
|
47
47
|
```css
|
|
48
|
+
html:has(body.camera-active),
|
|
48
49
|
body.camera-active,
|
|
50
|
+
body.camera-active #reactContainer,
|
|
51
|
+
body.camera-active .screen-container,
|
|
52
|
+
body.camera-active .active-screen,
|
|
53
|
+
body.camera-active .screen,
|
|
49
54
|
body.camera-active .layout,
|
|
50
55
|
body.camera-active .main,
|
|
51
56
|
body.camera-active .main-content,
|
|
52
|
-
body.camera-active .content
|
|
53
|
-
body.camera-active .screen,
|
|
54
|
-
body.camera-active .active-screen {
|
|
57
|
+
body.camera-active .content {
|
|
55
58
|
background: transparent !important;
|
|
56
59
|
}
|
|
57
60
|
```
|
|
@@ -78,6 +81,25 @@ Add to the app's `Info.plist`:
|
|
|
78
81
|
$parameters.ImageBase64 = photo.base64;
|
|
79
82
|
```
|
|
80
83
|
|
|
84
|
+
### Card detection (Android)
|
|
85
|
+
|
|
86
|
+
After `start()`, call `startDetection()`. A red rectangle is drawn around the
|
|
87
|
+
card (between the camera and the WebView) and its text is read on-device with
|
|
88
|
+
ML Kit. `cardDetected` fires once per card, when the same collector number (or
|
|
89
|
+
name) was read on `stableFrames` consecutive runs. Remove the card from view
|
|
90
|
+
for ~1.5s to scan the same card again.
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
const { CardScanner } = Capacitor.Plugins;
|
|
94
|
+
await CardScanner.addListener('cardDetected', (card) => {
|
|
95
|
+
// card.name, card.collectorNumber ('025/165'), card.text, card.lines, card.cardBox, card.image
|
|
96
|
+
});
|
|
97
|
+
await CardScanner.startDetection({ intervalMs: 300, stableFrames: 2, includeImage: false });
|
|
98
|
+
// ...
|
|
99
|
+
await CardScanner.stopDetection();
|
|
100
|
+
await CardScanner.removeAllListeners();
|
|
101
|
+
```
|
|
102
|
+
|
|
81
103
|
## API
|
|
82
104
|
|
|
83
105
|
<docgen-index>
|
|
@@ -88,6 +110,10 @@ Add to the app's `Info.plist`:
|
|
|
88
110
|
* [`setTorch(...)`](#settorch)
|
|
89
111
|
* [`setZoom(...)`](#setzoom)
|
|
90
112
|
* [`flip()`](#flip)
|
|
113
|
+
* [`startDetection(...)`](#startdetection)
|
|
114
|
+
* [`stopDetection()`](#stopdetection)
|
|
115
|
+
* [`addListener('cardDetected', ...)`](#addlistenercarddetected-)
|
|
116
|
+
* [`removeAllListeners()`](#removealllisteners)
|
|
91
117
|
* [`checkPermissions()`](#checkpermissions)
|
|
92
118
|
* [`requestPermissions()`](#requestpermissions)
|
|
93
119
|
* [Interfaces](#interfaces)
|
|
@@ -184,6 +210,60 @@ Switches between the rear and front camera.
|
|
|
184
210
|
--------------------
|
|
185
211
|
|
|
186
212
|
|
|
213
|
+
### startDetection(...)
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
startDetection(options?: DetectionOptions | undefined) => Promise<void>
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Starts looking for cards: draws a red rectangle around the card and reads
|
|
220
|
+
its text, emitting `cardDetected`. Requires `start()` first. Android only.
|
|
221
|
+
|
|
222
|
+
| Param | Type |
|
|
223
|
+
| ------------- | ------------------------------------------------------------- |
|
|
224
|
+
| **`options`** | <code><a href="#detectionoptions">DetectionOptions</a></code> |
|
|
225
|
+
|
|
226
|
+
--------------------
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
### stopDetection()
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
stopDetection() => Promise<void>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Stops card detection and hides the rectangle.
|
|
236
|
+
|
|
237
|
+
--------------------
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
### addListener('cardDetected', ...)
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
addListener(eventName: 'cardDetected', listener: (event: CardDetectedEvent) => void) => Promise<PluginListenerHandle>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Fired once per card, when its text has been read consistently.
|
|
247
|
+
|
|
248
|
+
| Param | Type |
|
|
249
|
+
| --------------- | ----------------------------------------------------------------------------------- |
|
|
250
|
+
| **`eventName`** | <code>'cardDetected'</code> |
|
|
251
|
+
| **`listener`** | <code>(event: <a href="#carddetectedevent">CardDetectedEvent</a>) => void</code> |
|
|
252
|
+
|
|
253
|
+
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
254
|
+
|
|
255
|
+
--------------------
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
### removeAllListeners()
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
removeAllListeners() => Promise<void>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
--------------------
|
|
265
|
+
|
|
266
|
+
|
|
187
267
|
### checkPermissions()
|
|
188
268
|
|
|
189
269
|
```typescript
|
|
@@ -246,6 +326,59 @@ requestPermissions() => Promise<PermissionStatus>
|
|
|
246
326
|
| **`ratio`** | <code>number</code> | Zoom ratio, clamped between the camera's min and max. |
|
|
247
327
|
|
|
248
328
|
|
|
329
|
+
#### DetectionOptions
|
|
330
|
+
|
|
331
|
+
| Prop | Type | Description | Default |
|
|
332
|
+
| --------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- |
|
|
333
|
+
| **`intervalMs`** | <code>number</code> | Minimum time between two OCR runs, in milliseconds. The red rectangle is updated on every frame regardless. | <code>300</code> |
|
|
334
|
+
| **`stableFrames`** | <code>number</code> | How many consecutive OCR runs must read the same card before `cardDetected` fires. | <code>2</code> |
|
|
335
|
+
| **`includeImage`** | <code>boolean</code> | Include a JPEG of the detected card in the event. | <code>false</code> |
|
|
336
|
+
| **`cardAspectRatio`** | <code>number</code> | Expected width / height of an upright card. Only upright cards are detected; a card lying sideways falls outside the tolerance. | <code>0.716 (63 x 88 mm)</code> |
|
|
337
|
+
| **`aspectTolerance`** | <code>number</code> | Allowed relative deviation from `cardAspectRatio` (0.15 = ±15%). Increase it if tilted cards are not detected. | <code>0.15</code> |
|
|
338
|
+
| **`minCardArea`** | <code>number</code> | Smallest card size, as a fraction of the camera frame (0-1). | <code>0.08</code> |
|
|
339
|
+
| **`maxCardArea`** | <code>number</code> | Largest card size, as a fraction of the camera frame (0-1). | <code>0.9</code> |
|
|
340
|
+
| **`requireCard`** | <code>boolean</code> | Only read text when a card-shaped object is visible. When false, the whole frame is read if no card is found. | <code>true</code> |
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
#### PluginListenerHandle
|
|
344
|
+
|
|
345
|
+
| Prop | Type |
|
|
346
|
+
| ------------ | ----------------------------------------- |
|
|
347
|
+
| **`remove`** | <code>() => Promise<void></code> |
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
#### CardDetectedEvent
|
|
351
|
+
|
|
352
|
+
| Prop | Type | Description |
|
|
353
|
+
| --------------------- | ----------------------------------- | ---------------------------------------------------------------------------- |
|
|
354
|
+
| **`name`** | <code>string</code> | Best guess for the card name (topmost line of text on the card). |
|
|
355
|
+
| **`collectorNumber`** | <code>string</code> | Collector number, e.g. `025/165`. |
|
|
356
|
+
| **`text`** | <code>string</code> | All text read on the card. |
|
|
357
|
+
| **`lines`** | <code>TextLine[]</code> | |
|
|
358
|
+
| **`cardBox`** | <code><a href="#box">Box</a></code> | Where the card is in the camera frame. Missing if no card outline was found. |
|
|
359
|
+
| **`image`** | <code>string</code> | Base64 JPEG of the card, when `includeImage` is true. |
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
#### TextLine
|
|
363
|
+
|
|
364
|
+
| Prop | Type | Description |
|
|
365
|
+
| ---------- | ----------------------------------- | ------------------------------- |
|
|
366
|
+
| **`text`** | <code>string</code> | |
|
|
367
|
+
| **`box`** | <code><a href="#box">Box</a></code> | Position inside the card image. |
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
#### Box
|
|
371
|
+
|
|
372
|
+
Normalized rectangle (0-1).
|
|
373
|
+
|
|
374
|
+
| Prop | Type |
|
|
375
|
+
| ------------ | ------------------- |
|
|
376
|
+
| **`x`** | <code>number</code> |
|
|
377
|
+
| **`y`** | <code>number</code> |
|
|
378
|
+
| **`width`** | <code>number</code> |
|
|
379
|
+
| **`height`** | <code>number</code> |
|
|
380
|
+
|
|
381
|
+
|
|
249
382
|
#### PermissionStatus
|
|
250
383
|
|
|
251
384
|
| Prop | Type |
|
package/android/build.gradle
CHANGED
|
@@ -61,4 +61,6 @@ dependencies {
|
|
|
61
61
|
implementation "androidx.camera:camera-camera2:$camerax_version"
|
|
62
62
|
implementation "androidx.camera:camera-lifecycle:$camerax_version"
|
|
63
63
|
implementation "androidx.camera:camera-view:$camerax_version"
|
|
64
|
+
implementation "com.google.mlkit:text-recognition:16.0.1"
|
|
65
|
+
implementation "com.google.mlkit:object-detection:17.0.2"
|
|
64
66
|
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
package pt.tcgclassifier.cardscanner;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap;
|
|
4
|
+
import android.graphics.Matrix;
|
|
5
|
+
import android.graphics.Rect;
|
|
6
|
+
import android.graphics.RectF;
|
|
7
|
+
import android.os.SystemClock;
|
|
8
|
+
import android.util.Base64;
|
|
9
|
+
import androidx.annotation.NonNull;
|
|
10
|
+
import androidx.annotation.Nullable;
|
|
11
|
+
import androidx.camera.core.ImageAnalysis;
|
|
12
|
+
import androidx.camera.core.ImageProxy;
|
|
13
|
+
import com.getcapacitor.JSArray;
|
|
14
|
+
import com.getcapacitor.JSObject;
|
|
15
|
+
import com.getcapacitor.Logger;
|
|
16
|
+
import com.google.android.gms.tasks.Tasks;
|
|
17
|
+
import com.google.mlkit.vision.common.InputImage;
|
|
18
|
+
import com.google.mlkit.vision.objects.DetectedObject;
|
|
19
|
+
import com.google.mlkit.vision.objects.ObjectDetection;
|
|
20
|
+
import com.google.mlkit.vision.objects.ObjectDetector;
|
|
21
|
+
import com.google.mlkit.vision.objects.defaults.ObjectDetectorOptions;
|
|
22
|
+
import com.google.mlkit.vision.text.Text;
|
|
23
|
+
import com.google.mlkit.vision.text.TextRecognition;
|
|
24
|
+
import com.google.mlkit.vision.text.TextRecognizer;
|
|
25
|
+
import com.google.mlkit.vision.text.latin.TextRecognizerOptions;
|
|
26
|
+
import java.io.ByteArrayOutputStream;
|
|
27
|
+
import java.util.List;
|
|
28
|
+
import java.util.regex.Matcher;
|
|
29
|
+
import java.util.regex.Pattern;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Locates the card in every camera frame (for the on-screen rectangle) and,
|
|
33
|
+
* every {@code intervalMs}, runs OCR inside it. A card is reported once the
|
|
34
|
+
* same collector number (or name) has been read on several consecutive runs.
|
|
35
|
+
*/
|
|
36
|
+
public class CardDetector implements ImageAnalysis.Analyzer {
|
|
37
|
+
|
|
38
|
+
public interface Listener {
|
|
39
|
+
void onCardDetected(JSObject result);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Tuning knobs, all overridable from JS through startDetection(). */
|
|
43
|
+
public static class Options {
|
|
44
|
+
|
|
45
|
+
/** Minimum time between two OCR runs. */
|
|
46
|
+
public long intervalMs = 300;
|
|
47
|
+
/** Consecutive OCR runs that must agree before a card is reported. */
|
|
48
|
+
public int stableFrames = 2;
|
|
49
|
+
/** Include a JPEG of the card in the event. */
|
|
50
|
+
public boolean includeImage = false;
|
|
51
|
+
/** Expected width / height of an upright card (63 x 88 mm). */
|
|
52
|
+
public float cardAspectRatio = 63f / 88f;
|
|
53
|
+
/** Allowed relative deviation from cardAspectRatio (0.15 = +/-15%). */
|
|
54
|
+
public float aspectTolerance = 0.15f;
|
|
55
|
+
/** Smallest card size, as a fraction of the frame area. */
|
|
56
|
+
public float minCardArea = 0.08f;
|
|
57
|
+
/** Largest card size, as a fraction of the frame area. */
|
|
58
|
+
public float maxCardArea = 0.9f;
|
|
59
|
+
/** Only run OCR when a card-shaped object is visible. */
|
|
60
|
+
public boolean requireCard = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public interface BoxListener {
|
|
64
|
+
/**
|
|
65
|
+
* @param box card bounds normalized (0-1) to the upright frame, or null when no card is visible
|
|
66
|
+
*/
|
|
67
|
+
void onBox(@Nullable RectF box, int frameWidth, int frameHeight);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// e.g. "025/165", "SV001/SV122", "TG05/TG30", "GG01/GG70"
|
|
71
|
+
private static final Pattern NUMBER = Pattern.compile("\\b([A-Za-z]{0,4}\\d{1,3}[a-z]?)\\s*/\\s*([A-Za-z]{0,4}\\d{1,3})\\b");
|
|
72
|
+
private static final Pattern HEADER = Pattern.compile(
|
|
73
|
+
"^(basic|stage\\s*\\d|trainer|item|supporter|stadium|energy|pok[eé]mon|evolves|hp\\b)",
|
|
74
|
+
Pattern.CASE_INSENSITIVE
|
|
75
|
+
);
|
|
76
|
+
private static final Pattern HP_SUFFIX = Pattern.compile("\\s*HP\\s*\\d+\\s*$", Pattern.CASE_INSENSITIVE);
|
|
77
|
+
private static final Pattern LETTER = Pattern.compile("\\p{L}");
|
|
78
|
+
/** After this long without reading a card, the same card can be reported again. */
|
|
79
|
+
private static final long RESET_AFTER_MS = 1500;
|
|
80
|
+
/** Extra margin around the detected card before running OCR. */
|
|
81
|
+
private static final float CROP_MARGIN = 0.04f;
|
|
82
|
+
|
|
83
|
+
private final ObjectDetector objectDetector = ObjectDetection.getClient(
|
|
84
|
+
new ObjectDetectorOptions.Builder().setDetectorMode(ObjectDetectorOptions.STREAM_MODE).build()
|
|
85
|
+
);
|
|
86
|
+
private final TextRecognizer recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS);
|
|
87
|
+
private final Options options;
|
|
88
|
+
private final Listener listener;
|
|
89
|
+
private volatile BoxListener boxListener;
|
|
90
|
+
|
|
91
|
+
// Only touched from the analyzer thread.
|
|
92
|
+
private long lastOcrAt;
|
|
93
|
+
private long lastSeenAt;
|
|
94
|
+
private String candidateKey;
|
|
95
|
+
private int candidateCount;
|
|
96
|
+
private String lastEmittedKey;
|
|
97
|
+
|
|
98
|
+
public CardDetector(Options options, Listener listener) {
|
|
99
|
+
this.options = options;
|
|
100
|
+
this.options.stableFrames = Math.max(1, options.stableFrames);
|
|
101
|
+
this.listener = listener;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public void setBoxListener(BoxListener boxListener) {
|
|
105
|
+
this.boxListener = boxListener;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public void close() {
|
|
109
|
+
objectDetector.close();
|
|
110
|
+
recognizer.close();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@Override
|
|
114
|
+
public void analyze(@NonNull ImageProxy image) {
|
|
115
|
+
long now = SystemClock.elapsedRealtime();
|
|
116
|
+
int rotation = image.getImageInfo().getRotationDegrees();
|
|
117
|
+
Bitmap raw;
|
|
118
|
+
try {
|
|
119
|
+
raw = image.toBitmap();
|
|
120
|
+
} finally {
|
|
121
|
+
image.close();
|
|
122
|
+
}
|
|
123
|
+
int frameW = rotation % 180 == 0 ? raw.getWidth() : raw.getHeight();
|
|
124
|
+
int frameH = rotation % 180 == 0 ? raw.getHeight() : raw.getWidth();
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
List<DetectedObject> objects = Tasks.await(objectDetector.process(InputImage.fromBitmap(raw, rotation)));
|
|
128
|
+
Rect card = pickCard(objects, frameW, frameH);
|
|
129
|
+
RectF cardBox = card == null ? null : new RectF(
|
|
130
|
+
card.left / (float) frameW,
|
|
131
|
+
card.top / (float) frameH,
|
|
132
|
+
card.right / (float) frameW,
|
|
133
|
+
card.bottom / (float) frameH
|
|
134
|
+
);
|
|
135
|
+
BoxListener bl = boxListener;
|
|
136
|
+
if (bl != null) bl.onBox(cardBox, frameW, frameH);
|
|
137
|
+
|
|
138
|
+
if (now - lastOcrAt < options.intervalMs) return;
|
|
139
|
+
lastOcrAt = now;
|
|
140
|
+
|
|
141
|
+
if (card == null && options.requireCard) {
|
|
142
|
+
noCardRead(now);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
Bitmap upright = rotate(raw, rotation);
|
|
147
|
+
Bitmap region = card == null ? upright : crop(upright, card);
|
|
148
|
+
Text text = Tasks.await(recognizer.process(InputImage.fromBitmap(region, 0)));
|
|
149
|
+
handle(text, region, cardBox, now);
|
|
150
|
+
} catch (Exception e) {
|
|
151
|
+
Logger.error("CardDetector", "Detection failed", e);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The biggest detected object with the size and upright proportions of a card. */
|
|
156
|
+
@Nullable
|
|
157
|
+
private Rect pickCard(List<DetectedObject> objects, int frameW, int frameH) {
|
|
158
|
+
float frameArea = (float) frameW * frameH;
|
|
159
|
+
float minRatio = options.cardAspectRatio * (1f - options.aspectTolerance);
|
|
160
|
+
float maxRatio = options.cardAspectRatio * (1f + options.aspectTolerance);
|
|
161
|
+
Rect best = null;
|
|
162
|
+
long bestArea = 0;
|
|
163
|
+
for (DetectedObject o : objects) {
|
|
164
|
+
Rect r = new Rect(o.getBoundingBox());
|
|
165
|
+
if (!r.intersect(0, 0, frameW, frameH) || r.height() == 0) continue;
|
|
166
|
+
|
|
167
|
+
long area = (long) r.width() * r.height();
|
|
168
|
+
float fraction = area / frameArea;
|
|
169
|
+
if (fraction < options.minCardArea || fraction > options.maxCardArea) continue;
|
|
170
|
+
|
|
171
|
+
float ratio = r.width() / (float) r.height();
|
|
172
|
+
if (ratio < minRatio || ratio > maxRatio) continue;
|
|
173
|
+
|
|
174
|
+
if (area > bestArea) {
|
|
175
|
+
best = r;
|
|
176
|
+
bestArea = area;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return best;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private static Bitmap rotate(Bitmap bitmap, int rotation) {
|
|
183
|
+
if (rotation == 0) return bitmap;
|
|
184
|
+
Matrix matrix = new Matrix();
|
|
185
|
+
matrix.postRotate(rotation);
|
|
186
|
+
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private static Bitmap crop(Bitmap bitmap, Rect card) {
|
|
190
|
+
int mx = Math.round(card.width() * CROP_MARGIN);
|
|
191
|
+
int my = Math.round(card.height() * CROP_MARGIN);
|
|
192
|
+
int left = Math.max(0, card.left - mx);
|
|
193
|
+
int top = Math.max(0, card.top - my);
|
|
194
|
+
int right = Math.min(bitmap.getWidth(), card.right + mx);
|
|
195
|
+
int bottom = Math.min(bitmap.getHeight(), card.bottom + my);
|
|
196
|
+
return Bitmap.createBitmap(bitmap, left, top, right - left, bottom - top);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private void handle(Text text, Bitmap region, @Nullable RectF cardBox, long now) {
|
|
200
|
+
float w = region.getWidth();
|
|
201
|
+
float h = region.getHeight();
|
|
202
|
+
JSArray lines = new JSArray();
|
|
203
|
+
String number = null;
|
|
204
|
+
String name = null;
|
|
205
|
+
int nameTop = Integer.MAX_VALUE;
|
|
206
|
+
|
|
207
|
+
for (Text.TextBlock block : text.getTextBlocks()) {
|
|
208
|
+
for (Text.Line line : block.getLines()) {
|
|
209
|
+
String s = line.getText().trim();
|
|
210
|
+
Rect r = line.getBoundingBox();
|
|
211
|
+
if (s.isEmpty() || r == null) continue;
|
|
212
|
+
|
|
213
|
+
JSObject item = new JSObject();
|
|
214
|
+
item.put("text", s);
|
|
215
|
+
item.put("box", box(r.left / w, r.top / h, r.width() / w, r.height() / h));
|
|
216
|
+
lines.put(item);
|
|
217
|
+
|
|
218
|
+
if (number == null) {
|
|
219
|
+
Matcher m = NUMBER.matcher(s);
|
|
220
|
+
if (m.find()) number = m.group(1) + "/" + m.group(2);
|
|
221
|
+
}
|
|
222
|
+
if (r.top < nameTop && isNameCandidate(s)) {
|
|
223
|
+
name = HP_SUFFIX.matcher(s).replaceAll("").trim();
|
|
224
|
+
nameTop = r.top;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
String key = number != null ? number : name;
|
|
230
|
+
if (key == null) {
|
|
231
|
+
noCardRead(now);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
lastSeenAt = now;
|
|
235
|
+
|
|
236
|
+
if (key.equals(candidateKey)) {
|
|
237
|
+
candidateCount++;
|
|
238
|
+
} else {
|
|
239
|
+
candidateKey = key;
|
|
240
|
+
candidateCount = 1;
|
|
241
|
+
}
|
|
242
|
+
if (candidateCount < options.stableFrames || key.equals(lastEmittedKey)) return;
|
|
243
|
+
lastEmittedKey = key;
|
|
244
|
+
|
|
245
|
+
JSObject result = new JSObject();
|
|
246
|
+
result.put("name", name);
|
|
247
|
+
result.put("collectorNumber", number);
|
|
248
|
+
result.put("text", text.getText());
|
|
249
|
+
result.put("lines", lines);
|
|
250
|
+
if (cardBox != null) {
|
|
251
|
+
result.put("cardBox", box(cardBox.left, cardBox.top, cardBox.width(), cardBox.height()));
|
|
252
|
+
}
|
|
253
|
+
if (options.includeImage) {
|
|
254
|
+
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
255
|
+
region.compress(Bitmap.CompressFormat.JPEG, 80, out);
|
|
256
|
+
result.put("image", Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP));
|
|
257
|
+
}
|
|
258
|
+
listener.onCardDetected(result);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private void noCardRead(long now) {
|
|
262
|
+
candidateKey = null;
|
|
263
|
+
candidateCount = 0;
|
|
264
|
+
if (now - lastSeenAt > RESET_AFTER_MS) lastEmittedKey = null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private static JSObject box(float x, float y, float width, float height) {
|
|
268
|
+
JSObject box = new JSObject();
|
|
269
|
+
box.put("x", x);
|
|
270
|
+
box.put("y", y);
|
|
271
|
+
box.put("width", width);
|
|
272
|
+
box.put("height", height);
|
|
273
|
+
return box;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private static boolean isNameCandidate(String s) {
|
|
277
|
+
if (HEADER.matcher(s).find()) return false;
|
|
278
|
+
Matcher m = LETTER.matcher(s);
|
|
279
|
+
int letters = 0;
|
|
280
|
+
while (m.find()) letters++;
|
|
281
|
+
return letters >= 3;
|
|
282
|
+
}
|
|
283
|
+
}
|