capacitor-card-scanner 0.2.0 → 0.3.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 +12 -10
- package/android/src/main/java/pt/tcgclassifier/cardscanner/CardDetector.java +70 -22
- package/android/src/main/java/pt/tcgclassifier/cardscanner/CardScanner.java +4 -2
- package/android/src/main/java/pt/tcgclassifier/cardscanner/CardScannerPlugin.java +166 -164
- package/android/src/main/java/pt/tcgclassifier/cardscanner/DetectionOverlay.java +127 -82
- package/dist/docs.json +24 -0
- package/dist/esm/definitions.d.ts +14 -0
- package/dist/esm/definitions.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -328,16 +328,18 @@ requestPermissions() => Promise<PermissionStatus>
|
|
|
328
328
|
|
|
329
329
|
#### DetectionOptions
|
|
330
330
|
|
|
331
|
-
| Prop | Type | Description
|
|
332
|
-
| --------------------- | -------------------- |
|
|
333
|
-
| **`intervalMs`** | <code>number</code> | Minimum time between two OCR runs, in milliseconds. The red rectangle is updated on every frame regardless.
|
|
334
|
-
| **`stableFrames`** | <code>number</code> | How many consecutive OCR runs must read the same card before `cardDetected` fires.
|
|
335
|
-
| **`includeImage`** | <code>boolean</code> | Include a JPEG of the detected card in the event.
|
|
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.
|
|
337
|
-
| **`aspectTolerance`** | <code>number</code> | Allowed relative deviation from `cardAspectRatio` (0.15 = ±15%). Increase it if tilted cards are not detected.
|
|
338
|
-
| **`minCardArea`** | <code>number</code> | Smallest card size, as a fraction of the camera frame (0-1).
|
|
339
|
-
| **`maxCardArea`** | <code>number</code> | Largest card size, as a fraction of the camera frame (0-1).
|
|
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.
|
|
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
|
+
| **`debug`** | <code>boolean</code> | Every object found is always outlined: red when accepted as a card, grey when rejected. With `debug`, each outline is also labelled with its area (`a`), width/height ratio (`r`) and the filter that rejected it. Use it to tune the filters above. | <code>false</code> |
|
|
342
|
+
| **`showRejected`** | <code>boolean</code> | Outline (in grey) the objects that were not accepted as a card. The accepted card is always outlined in red. | <code>true</code> |
|
|
341
343
|
|
|
342
344
|
|
|
343
345
|
#### PluginListenerHandle
|
|
@@ -24,7 +24,9 @@ import com.google.mlkit.vision.text.TextRecognition;
|
|
|
24
24
|
import com.google.mlkit.vision.text.TextRecognizer;
|
|
25
25
|
import com.google.mlkit.vision.text.latin.TextRecognizerOptions;
|
|
26
26
|
import java.io.ByteArrayOutputStream;
|
|
27
|
+
import java.util.ArrayList;
|
|
27
28
|
import java.util.List;
|
|
29
|
+
import java.util.Locale;
|
|
28
30
|
import java.util.regex.Matcher;
|
|
29
31
|
import java.util.regex.Pattern;
|
|
30
32
|
|
|
@@ -58,13 +60,44 @@ public class CardDetector implements ImageAnalysis.Analyzer {
|
|
|
58
60
|
public float maxCardArea = 0.9f;
|
|
59
61
|
/** Only run OCR when a card-shaped object is visible. */
|
|
60
62
|
public boolean requireCard = true;
|
|
63
|
+
/** Label every detected object with its area/ratio and the filter that rejected it. */
|
|
64
|
+
public boolean debug = false;
|
|
65
|
+
/** Outline (in grey) the objects that were not accepted as a card. */
|
|
66
|
+
public boolean showRejected = true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** An object found by ML Kit, with the values the card filters were checked against. */
|
|
70
|
+
public static class Candidate {
|
|
71
|
+
|
|
72
|
+
/** Bounds normalized (0-1) to the upright frame. */
|
|
73
|
+
public final RectF box;
|
|
74
|
+
/** Fraction of the frame area. */
|
|
75
|
+
public final float area;
|
|
76
|
+
/** Width / height. */
|
|
77
|
+
public final float ratio;
|
|
78
|
+
/** Null when accepted, otherwise the filter that rejected it ("area" or "ratio"). */
|
|
79
|
+
@Nullable
|
|
80
|
+
public final String rejectedBy;
|
|
81
|
+
|
|
82
|
+
Candidate(RectF box, float area, float ratio, @Nullable String rejectedBy) {
|
|
83
|
+
this.box = box;
|
|
84
|
+
this.area = area;
|
|
85
|
+
this.ratio = ratio;
|
|
86
|
+
this.rejectedBy = rejectedBy;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public String label() {
|
|
90
|
+
String s = String.format(Locale.US, "a=%.2f r=%.2f", area, ratio);
|
|
91
|
+
return rejectedBy == null ? s : s + " x" + rejectedBy;
|
|
92
|
+
}
|
|
61
93
|
}
|
|
62
94
|
|
|
63
95
|
public interface BoxListener {
|
|
64
96
|
/**
|
|
65
|
-
* @param
|
|
97
|
+
* @param card card bounds normalized (0-1) to the upright frame, or null when no card is visible
|
|
98
|
+
* @param candidates every detected object, accepted or not
|
|
66
99
|
*/
|
|
67
|
-
void
|
|
100
|
+
void onDetections(@Nullable RectF card, List<Candidate> candidates, int frameWidth, int frameHeight);
|
|
68
101
|
}
|
|
69
102
|
|
|
70
103
|
// e.g. "025/165", "SV001/SV122", "TG05/TG30", "GG01/GG70"
|
|
@@ -101,6 +134,14 @@ public class CardDetector implements ImageAnalysis.Analyzer {
|
|
|
101
134
|
this.listener = listener;
|
|
102
135
|
}
|
|
103
136
|
|
|
137
|
+
public boolean isDebug() {
|
|
138
|
+
return options.debug;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public boolean isShowRejected() {
|
|
142
|
+
return options.showRejected;
|
|
143
|
+
}
|
|
144
|
+
|
|
104
145
|
public void setBoxListener(BoxListener boxListener) {
|
|
105
146
|
this.boxListener = boxListener;
|
|
106
147
|
}
|
|
@@ -113,27 +154,23 @@ public class CardDetector implements ImageAnalysis.Analyzer {
|
|
|
113
154
|
@Override
|
|
114
155
|
public void analyze(@NonNull ImageProxy image) {
|
|
115
156
|
long now = SystemClock.elapsedRealtime();
|
|
116
|
-
|
|
117
|
-
Bitmap raw;
|
|
157
|
+
Bitmap upright;
|
|
118
158
|
try {
|
|
119
|
-
|
|
159
|
+
// Rotate once, so both detectors and all coordinates share the same upright frame.
|
|
160
|
+
upright = rotate(image.toBitmap(), image.getImageInfo().getRotationDegrees());
|
|
120
161
|
} finally {
|
|
121
162
|
image.close();
|
|
122
163
|
}
|
|
123
|
-
int frameW =
|
|
124
|
-
int frameH =
|
|
164
|
+
int frameW = upright.getWidth();
|
|
165
|
+
int frameH = upright.getHeight();
|
|
125
166
|
|
|
126
167
|
try {
|
|
127
|
-
List<DetectedObject> objects = Tasks.await(objectDetector.process(InputImage.fromBitmap(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
card.top / (float) frameH,
|
|
132
|
-
card.right / (float) frameW,
|
|
133
|
-
card.bottom / (float) frameH
|
|
134
|
-
);
|
|
168
|
+
List<DetectedObject> objects = Tasks.await(objectDetector.process(InputImage.fromBitmap(upright, 0)));
|
|
169
|
+
List<Candidate> candidates = new ArrayList<>();
|
|
170
|
+
Rect card = pickCard(objects, frameW, frameH, candidates);
|
|
171
|
+
RectF cardBox = card == null ? null : normalize(card, frameW, frameH);
|
|
135
172
|
BoxListener bl = boxListener;
|
|
136
|
-
if (bl != null) bl.
|
|
173
|
+
if (bl != null) bl.onDetections(cardBox, candidates, frameW, frameH);
|
|
137
174
|
|
|
138
175
|
if (now - lastOcrAt < options.intervalMs) return;
|
|
139
176
|
lastOcrAt = now;
|
|
@@ -143,7 +180,6 @@ public class CardDetector implements ImageAnalysis.Analyzer {
|
|
|
143
180
|
return;
|
|
144
181
|
}
|
|
145
182
|
|
|
146
|
-
Bitmap upright = rotate(raw, rotation);
|
|
147
183
|
Bitmap region = card == null ? upright : crop(upright, card);
|
|
148
184
|
Text text = Tasks.await(recognizer.process(InputImage.fromBitmap(region, 0)));
|
|
149
185
|
handle(text, region, cardBox, now);
|
|
@@ -152,9 +188,12 @@ public class CardDetector implements ImageAnalysis.Analyzer {
|
|
|
152
188
|
}
|
|
153
189
|
}
|
|
154
190
|
|
|
155
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* The biggest detected object with the size and upright proportions of a card.
|
|
193
|
+
* Every object, accepted or not, is added to {@code candidates}.
|
|
194
|
+
*/
|
|
156
195
|
@Nullable
|
|
157
|
-
private Rect pickCard(List<DetectedObject> objects, int frameW, int frameH) {
|
|
196
|
+
private Rect pickCard(List<DetectedObject> objects, int frameW, int frameH, List<Candidate> candidates) {
|
|
158
197
|
float frameArea = (float) frameW * frameH;
|
|
159
198
|
float minRatio = options.cardAspectRatio * (1f - options.aspectTolerance);
|
|
160
199
|
float maxRatio = options.cardAspectRatio * (1f + options.aspectTolerance);
|
|
@@ -166,10 +205,15 @@ public class CardDetector implements ImageAnalysis.Analyzer {
|
|
|
166
205
|
|
|
167
206
|
long area = (long) r.width() * r.height();
|
|
168
207
|
float fraction = area / frameArea;
|
|
169
|
-
if (fraction < options.minCardArea || fraction > options.maxCardArea) continue;
|
|
170
|
-
|
|
171
208
|
float ratio = r.width() / (float) r.height();
|
|
172
|
-
|
|
209
|
+
String rejectedBy = null;
|
|
210
|
+
if (fraction < options.minCardArea || fraction > options.maxCardArea) {
|
|
211
|
+
rejectedBy = "area";
|
|
212
|
+
} else if (ratio < minRatio || ratio > maxRatio) {
|
|
213
|
+
rejectedBy = "ratio";
|
|
214
|
+
}
|
|
215
|
+
candidates.add(new Candidate(normalize(r, frameW, frameH), fraction, ratio, rejectedBy));
|
|
216
|
+
if (rejectedBy != null) continue;
|
|
173
217
|
|
|
174
218
|
if (area > bestArea) {
|
|
175
219
|
best = r;
|
|
@@ -179,6 +223,10 @@ public class CardDetector implements ImageAnalysis.Analyzer {
|
|
|
179
223
|
return best;
|
|
180
224
|
}
|
|
181
225
|
|
|
226
|
+
private static RectF normalize(Rect r, int frameW, int frameH) {
|
|
227
|
+
return new RectF(r.left / (float) frameW, r.top / (float) frameH, r.right / (float) frameW, r.bottom / (float) frameH);
|
|
228
|
+
}
|
|
229
|
+
|
|
182
230
|
private static Bitmap rotate(Bitmap bitmap, int rotation) {
|
|
183
231
|
if (rotation == 0) return bitmap;
|
|
184
232
|
Matrix matrix = new Matrix();
|
|
@@ -151,10 +151,12 @@ public class CardScanner {
|
|
|
151
151
|
closeDetector();
|
|
152
152
|
detector = newDetector;
|
|
153
153
|
DetectionOverlay target = overlay;
|
|
154
|
-
|
|
154
|
+
target.setShowLabels(newDetector.isDebug());
|
|
155
|
+
target.setShowRejected(newDetector.isShowRejected());
|
|
156
|
+
detector.setBoxListener((card, candidates, frameW, frameH) ->
|
|
155
157
|
target.post(() -> {
|
|
156
158
|
// Ignore frames that arrive after detection was stopped or restarted.
|
|
157
|
-
if (detector == newDetector) target.
|
|
159
|
+
if (detector == newDetector) target.setDetections(card, candidates, frameW, frameH);
|
|
158
160
|
})
|
|
159
161
|
);
|
|
160
162
|
bind();
|
|
@@ -1,164 +1,166 @@
|
|
|
1
|
-
package pt.tcgclassifier.cardscanner;
|
|
2
|
-
|
|
3
|
-
import android.Manifest;
|
|
4
|
-
import com.getcapacitor.JSObject;
|
|
5
|
-
import com.getcapacitor.PermissionState;
|
|
6
|
-
import com.getcapacitor.Plugin;
|
|
7
|
-
import com.getcapacitor.PluginCall;
|
|
8
|
-
import com.getcapacitor.PluginMethod;
|
|
9
|
-
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
10
|
-
import com.getcapacitor.annotation.Permission;
|
|
11
|
-
import com.getcapacitor.annotation.PermissionCallback;
|
|
12
|
-
|
|
13
|
-
@CapacitorPlugin(name = "CardScanner", permissions = { @Permission(strings = { Manifest.permission.CAMERA }, alias = CardScannerPlugin.CAMERA) })
|
|
14
|
-
public class CardScannerPlugin extends Plugin {
|
|
15
|
-
|
|
16
|
-
static final String CAMERA = "camera";
|
|
17
|
-
|
|
18
|
-
private CardScanner implementation;
|
|
19
|
-
|
|
20
|
-
@Override
|
|
21
|
-
public void load() {
|
|
22
|
-
implementation = new CardScanner(getActivity(), getBridge().getWebView());
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
@Override
|
|
26
|
-
protected void handleOnDestroy() {
|
|
27
|
-
getActivity().runOnUiThread(() -> implementation.stop());
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
@PluginMethod
|
|
31
|
-
public void start(PluginCall call) {
|
|
32
|
-
if (getPermissionState(CAMERA) != PermissionState.GRANTED) {
|
|
33
|
-
requestPermissionForAlias(CAMERA, call, "cameraPermsCallback");
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
doStart(call);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
@PermissionCallback
|
|
40
|
-
private void cameraPermsCallback(PluginCall call) {
|
|
41
|
-
if (getPermissionState(CAMERA) == PermissionState.GRANTED) {
|
|
42
|
-
doStart(call);
|
|
43
|
-
} else {
|
|
44
|
-
call.reject("Camera permission denied", "PERMISSION_DENIED");
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private void doStart(PluginCall call) {
|
|
49
|
-
boolean front = "front".equals(call.getString("position", "rear"));
|
|
50
|
-
getActivity().runOnUiThread(() -> implementation.start(front, voidCallback(call)));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
@PluginMethod
|
|
54
|
-
public void stop(PluginCall call) {
|
|
55
|
-
getActivity().runOnUiThread(() -> {
|
|
56
|
-
implementation.stop();
|
|
57
|
-
call.resolve();
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
@PluginMethod
|
|
62
|
-
public void flip(PluginCall call) {
|
|
63
|
-
if (!ensureRunning(call)) return;
|
|
64
|
-
getActivity().runOnUiThread(() -> implementation.flip(voidCallback(call)));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
@PluginMethod
|
|
68
|
-
public void capture(PluginCall call) {
|
|
69
|
-
if (!ensureRunning(call)) return;
|
|
70
|
-
int quality = Math.max(0, Math.min(100, call.getInt("quality", 85)));
|
|
71
|
-
implementation.capture(
|
|
72
|
-
quality,
|
|
73
|
-
new CardScanner.Callback<>() {
|
|
74
|
-
@Override
|
|
75
|
-
public void onSuccess(CardScanner.Photo photo) {
|
|
76
|
-
JSObject ret = new JSObject();
|
|
77
|
-
ret.put("base64", photo.base64);
|
|
78
|
-
ret.put("width", photo.width);
|
|
79
|
-
ret.put("height", photo.height);
|
|
80
|
-
call.resolve(ret);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@Override
|
|
84
|
-
public void onError(String message, Exception e) {
|
|
85
|
-
call.reject(message, e);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
@PluginMethod
|
|
92
|
-
public void startDetection(PluginCall call) {
|
|
93
|
-
if (!ensureRunning(call)) return;
|
|
94
|
-
CardDetector.Options options = new CardDetector.Options();
|
|
95
|
-
options.intervalMs = call.getInt("intervalMs", (int) options.intervalMs);
|
|
96
|
-
options.stableFrames = call.getInt("stableFrames", options.stableFrames);
|
|
97
|
-
options.includeImage = call.getBoolean("includeImage", options.includeImage);
|
|
98
|
-
options.cardAspectRatio = call.getFloat("cardAspectRatio", options.cardAspectRatio);
|
|
99
|
-
options.aspectTolerance = call.getFloat("aspectTolerance", options.aspectTolerance);
|
|
100
|
-
options.minCardArea = call.getFloat("minCardArea", options.minCardArea);
|
|
101
|
-
options.maxCardArea = call.getFloat("maxCardArea", options.maxCardArea);
|
|
102
|
-
options.requireCard = call.getBoolean("requireCard", options.requireCard);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
call
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
1
|
+
package pt.tcgclassifier.cardscanner;
|
|
2
|
+
|
|
3
|
+
import android.Manifest;
|
|
4
|
+
import com.getcapacitor.JSObject;
|
|
5
|
+
import com.getcapacitor.PermissionState;
|
|
6
|
+
import com.getcapacitor.Plugin;
|
|
7
|
+
import com.getcapacitor.PluginCall;
|
|
8
|
+
import com.getcapacitor.PluginMethod;
|
|
9
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
10
|
+
import com.getcapacitor.annotation.Permission;
|
|
11
|
+
import com.getcapacitor.annotation.PermissionCallback;
|
|
12
|
+
|
|
13
|
+
@CapacitorPlugin(name = "CardScanner", permissions = { @Permission(strings = { Manifest.permission.CAMERA }, alias = CardScannerPlugin.CAMERA) })
|
|
14
|
+
public class CardScannerPlugin extends Plugin {
|
|
15
|
+
|
|
16
|
+
static final String CAMERA = "camera";
|
|
17
|
+
|
|
18
|
+
private CardScanner implementation;
|
|
19
|
+
|
|
20
|
+
@Override
|
|
21
|
+
public void load() {
|
|
22
|
+
implementation = new CardScanner(getActivity(), getBridge().getWebView());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@Override
|
|
26
|
+
protected void handleOnDestroy() {
|
|
27
|
+
getActivity().runOnUiThread(() -> implementation.stop());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@PluginMethod
|
|
31
|
+
public void start(PluginCall call) {
|
|
32
|
+
if (getPermissionState(CAMERA) != PermissionState.GRANTED) {
|
|
33
|
+
requestPermissionForAlias(CAMERA, call, "cameraPermsCallback");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
doStart(call);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@PermissionCallback
|
|
40
|
+
private void cameraPermsCallback(PluginCall call) {
|
|
41
|
+
if (getPermissionState(CAMERA) == PermissionState.GRANTED) {
|
|
42
|
+
doStart(call);
|
|
43
|
+
} else {
|
|
44
|
+
call.reject("Camera permission denied", "PERMISSION_DENIED");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private void doStart(PluginCall call) {
|
|
49
|
+
boolean front = "front".equals(call.getString("position", "rear"));
|
|
50
|
+
getActivity().runOnUiThread(() -> implementation.start(front, voidCallback(call)));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@PluginMethod
|
|
54
|
+
public void stop(PluginCall call) {
|
|
55
|
+
getActivity().runOnUiThread(() -> {
|
|
56
|
+
implementation.stop();
|
|
57
|
+
call.resolve();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@PluginMethod
|
|
62
|
+
public void flip(PluginCall call) {
|
|
63
|
+
if (!ensureRunning(call)) return;
|
|
64
|
+
getActivity().runOnUiThread(() -> implementation.flip(voidCallback(call)));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@PluginMethod
|
|
68
|
+
public void capture(PluginCall call) {
|
|
69
|
+
if (!ensureRunning(call)) return;
|
|
70
|
+
int quality = Math.max(0, Math.min(100, call.getInt("quality", 85)));
|
|
71
|
+
implementation.capture(
|
|
72
|
+
quality,
|
|
73
|
+
new CardScanner.Callback<>() {
|
|
74
|
+
@Override
|
|
75
|
+
public void onSuccess(CardScanner.Photo photo) {
|
|
76
|
+
JSObject ret = new JSObject();
|
|
77
|
+
ret.put("base64", photo.base64);
|
|
78
|
+
ret.put("width", photo.width);
|
|
79
|
+
ret.put("height", photo.height);
|
|
80
|
+
call.resolve(ret);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@Override
|
|
84
|
+
public void onError(String message, Exception e) {
|
|
85
|
+
call.reject(message, e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@PluginMethod
|
|
92
|
+
public void startDetection(PluginCall call) {
|
|
93
|
+
if (!ensureRunning(call)) return;
|
|
94
|
+
CardDetector.Options options = new CardDetector.Options();
|
|
95
|
+
options.intervalMs = call.getInt("intervalMs", (int) options.intervalMs);
|
|
96
|
+
options.stableFrames = call.getInt("stableFrames", options.stableFrames);
|
|
97
|
+
options.includeImage = call.getBoolean("includeImage", options.includeImage);
|
|
98
|
+
options.cardAspectRatio = call.getFloat("cardAspectRatio", options.cardAspectRatio);
|
|
99
|
+
options.aspectTolerance = call.getFloat("aspectTolerance", options.aspectTolerance);
|
|
100
|
+
options.minCardArea = call.getFloat("minCardArea", options.minCardArea);
|
|
101
|
+
options.maxCardArea = call.getFloat("maxCardArea", options.maxCardArea);
|
|
102
|
+
options.requireCard = call.getBoolean("requireCard", options.requireCard);
|
|
103
|
+
options.debug = call.getBoolean("debug", options.debug);
|
|
104
|
+
options.showRejected = call.getBoolean("showRejected", options.showRejected);
|
|
105
|
+
|
|
106
|
+
CardDetector detector = new CardDetector(options, result -> notifyListeners("cardDetected", result));
|
|
107
|
+
getActivity().runOnUiThread(() -> {
|
|
108
|
+
try {
|
|
109
|
+
implementation.startDetection(detector);
|
|
110
|
+
call.resolve();
|
|
111
|
+
} catch (Exception e) {
|
|
112
|
+
detector.close();
|
|
113
|
+
call.reject("Failed to start detection: " + e.getMessage(), e);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@PluginMethod
|
|
119
|
+
public void stopDetection(PluginCall call) {
|
|
120
|
+
getActivity().runOnUiThread(() -> {
|
|
121
|
+
implementation.stopDetection();
|
|
122
|
+
call.resolve();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@PluginMethod
|
|
127
|
+
public void setTorch(PluginCall call) {
|
|
128
|
+
if (!ensureRunning(call)) return;
|
|
129
|
+
implementation.setTorch(Boolean.TRUE.equals(call.getBoolean("enabled", false)));
|
|
130
|
+
call.resolve();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@PluginMethod
|
|
134
|
+
public void setZoom(PluginCall call) {
|
|
135
|
+
if (!ensureRunning(call)) return;
|
|
136
|
+
Float ratio = call.getFloat("ratio");
|
|
137
|
+
if (ratio == null) {
|
|
138
|
+
call.reject("Missing 'ratio'");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
implementation.setZoom(ratio);
|
|
142
|
+
call.resolve();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private boolean ensureRunning(PluginCall call) {
|
|
146
|
+
if (!implementation.isRunning()) {
|
|
147
|
+
call.reject("Camera is not running", "NOT_RUNNING");
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private CardScanner.Callback<Void> voidCallback(PluginCall call) {
|
|
154
|
+
return new CardScanner.Callback<>() {
|
|
155
|
+
@Override
|
|
156
|
+
public void onSuccess(Void result) {
|
|
157
|
+
call.resolve();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@Override
|
|
161
|
+
public void onError(String message, Exception e) {
|
|
162
|
+
call.reject(message, e);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -1,82 +1,127 @@
|
|
|
1
|
-
package pt.tcgclassifier.cardscanner;
|
|
2
|
-
|
|
3
|
-
import android.content.Context;
|
|
4
|
-
import android.graphics.Canvas;
|
|
5
|
-
import android.graphics.Color;
|
|
6
|
-
import android.graphics.Paint;
|
|
7
|
-
import android.graphics.RectF;
|
|
8
|
-
import android.os.SystemClock;
|
|
9
|
-
import android.view.View;
|
|
10
|
-
import androidx.annotation.Nullable;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
private final
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
private
|
|
29
|
-
private
|
|
30
|
-
private
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
public
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1
|
+
package pt.tcgclassifier.cardscanner;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.graphics.Canvas;
|
|
5
|
+
import android.graphics.Color;
|
|
6
|
+
import android.graphics.Paint;
|
|
7
|
+
import android.graphics.RectF;
|
|
8
|
+
import android.os.SystemClock;
|
|
9
|
+
import android.view.View;
|
|
10
|
+
import androidx.annotation.Nullable;
|
|
11
|
+
import java.util.Collections;
|
|
12
|
+
import java.util.List;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Draws a red rectangle around the detected card. Sits between the camera
|
|
16
|
+
* preview and the (transparent) WebView, and maps frame coordinates the same
|
|
17
|
+
* way PreviewView's FILL_CENTER does. Objects that are not accepted as a card
|
|
18
|
+
* are drawn in grey; with labels on, each one shows its area/ratio.
|
|
19
|
+
*/
|
|
20
|
+
public class DetectionOverlay extends View {
|
|
21
|
+
|
|
22
|
+
/** Keep the rectangle this long after the card was last seen, to avoid flicker. */
|
|
23
|
+
private static final long HIDE_AFTER_MS = 300;
|
|
24
|
+
|
|
25
|
+
private final Paint cardPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
26
|
+
private final Paint rejectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
27
|
+
private final Paint labelPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
28
|
+
private final float radius;
|
|
29
|
+
private final float labelPadding;
|
|
30
|
+
private final RectF drawRect = new RectF();
|
|
31
|
+
|
|
32
|
+
@Nullable
|
|
33
|
+
private RectF card;
|
|
34
|
+
private List<CardDetector.Candidate> candidates = Collections.emptyList();
|
|
35
|
+
private int frameWidth;
|
|
36
|
+
private int frameHeight;
|
|
37
|
+
private boolean mirror;
|
|
38
|
+
private boolean showLabels;
|
|
39
|
+
private boolean showRejected = true;
|
|
40
|
+
private long lastCardAt;
|
|
41
|
+
|
|
42
|
+
public DetectionOverlay(Context context) {
|
|
43
|
+
super(context);
|
|
44
|
+
float density = context.getResources().getDisplayMetrics().density;
|
|
45
|
+
radius = 8 * density;
|
|
46
|
+
labelPadding = 4 * density;
|
|
47
|
+
|
|
48
|
+
cardPaint.setColor(Color.RED);
|
|
49
|
+
cardPaint.setStyle(Paint.Style.STROKE);
|
|
50
|
+
cardPaint.setStrokeWidth(3 * density);
|
|
51
|
+
|
|
52
|
+
rejectedPaint.setColor(Color.argb(200, 180, 180, 180));
|
|
53
|
+
rejectedPaint.setStyle(Paint.Style.STROKE);
|
|
54
|
+
rejectedPaint.setStrokeWidth(2 * density);
|
|
55
|
+
|
|
56
|
+
labelPaint.setColor(Color.WHITE);
|
|
57
|
+
labelPaint.setTextSize(13 * density);
|
|
58
|
+
labelPaint.setShadowLayer(3 * density, 0, 0, Color.BLACK);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public void setMirror(boolean mirror) {
|
|
62
|
+
this.mirror = mirror;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public void setShowLabels(boolean showLabels) {
|
|
66
|
+
this.showLabels = showLabels;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public void setShowRejected(boolean showRejected) {
|
|
70
|
+
this.showRejected = showRejected;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Must be called on the UI thread. */
|
|
74
|
+
public void setDetections(@Nullable RectF card, List<CardDetector.Candidate> candidates, int frameWidth, int frameHeight) {
|
|
75
|
+
long now = SystemClock.uptimeMillis();
|
|
76
|
+
if (card != null) {
|
|
77
|
+
lastCardAt = now;
|
|
78
|
+
this.card = card;
|
|
79
|
+
} else if (now - lastCardAt >= HIDE_AFTER_MS) {
|
|
80
|
+
this.card = null;
|
|
81
|
+
}
|
|
82
|
+
this.candidates = candidates;
|
|
83
|
+
this.frameWidth = frameWidth;
|
|
84
|
+
this.frameHeight = frameHeight;
|
|
85
|
+
invalidate();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
public void clear() {
|
|
89
|
+
card = null;
|
|
90
|
+
candidates = Collections.emptyList();
|
|
91
|
+
invalidate();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@Override
|
|
95
|
+
protected void onDraw(Canvas canvas) {
|
|
96
|
+
if (frameWidth == 0 || frameHeight == 0) return;
|
|
97
|
+
|
|
98
|
+
for (CardDetector.Candidate c : candidates) {
|
|
99
|
+
if (c.rejectedBy != null && !showRejected) continue;
|
|
100
|
+
map(c.box);
|
|
101
|
+
canvas.drawRoundRect(drawRect, radius, radius, c.rejectedBy == null ? cardPaint : rejectedPaint);
|
|
102
|
+
if (showLabels) {
|
|
103
|
+
canvas.drawText(c.label(), drawRect.left + labelPadding, drawRect.top - labelPadding, labelPaint);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (card != null) {
|
|
108
|
+
map(card);
|
|
109
|
+
canvas.drawRoundRect(drawRect, radius, radius, cardPaint);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Maps a normalized frame rectangle into drawRect, in view coordinates. */
|
|
114
|
+
private void map(RectF box) {
|
|
115
|
+
float vw = getWidth();
|
|
116
|
+
float vh = getHeight();
|
|
117
|
+
float scale = Math.max(vw / frameWidth, vh / frameHeight);
|
|
118
|
+
float sw = frameWidth * scale;
|
|
119
|
+
float sh = frameHeight * scale;
|
|
120
|
+
float dx = (vw - sw) / 2f;
|
|
121
|
+
float dy = (vh - sh) / 2f;
|
|
122
|
+
|
|
123
|
+
float left = mirror ? 1f - box.right : box.left;
|
|
124
|
+
float right = mirror ? 1f - box.left : box.right;
|
|
125
|
+
drawRect.set(dx + left * sw, dy + box.top * sh, dx + right * sw, dy + box.bottom * sh);
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/docs.json
CHANGED
|
@@ -396,6 +396,30 @@
|
|
|
396
396
|
"docs": "Only read text when a card-shaped object is visible. When false, the whole\nframe is read if no card is found.",
|
|
397
397
|
"complexTypes": [],
|
|
398
398
|
"type": "boolean | undefined"
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
"name": "debug",
|
|
402
|
+
"tags": [
|
|
403
|
+
{
|
|
404
|
+
"text": "false",
|
|
405
|
+
"name": "default"
|
|
406
|
+
}
|
|
407
|
+
],
|
|
408
|
+
"docs": "Every object found is always outlined: red when accepted as a card, grey\nwhen rejected. With `debug`, each outline is also labelled with its area\n(`a`), width/height ratio (`r`) and the filter that rejected it. Use it to\ntune the filters above.",
|
|
409
|
+
"complexTypes": [],
|
|
410
|
+
"type": "boolean | undefined"
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"name": "showRejected",
|
|
414
|
+
"tags": [
|
|
415
|
+
{
|
|
416
|
+
"text": "true",
|
|
417
|
+
"name": "default"
|
|
418
|
+
}
|
|
419
|
+
],
|
|
420
|
+
"docs": "Outline (in grey) the objects that were not accepted as a card.\nThe accepted card is always outlined in red.",
|
|
421
|
+
"complexTypes": [],
|
|
422
|
+
"type": "boolean | undefined"
|
|
399
423
|
}
|
|
400
424
|
]
|
|
401
425
|
},
|
|
@@ -76,6 +76,20 @@ export interface DetectionOptions {
|
|
|
76
76
|
* @default true
|
|
77
77
|
*/
|
|
78
78
|
requireCard?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Every object found is always outlined: red when accepted as a card, grey
|
|
81
|
+
* when rejected. With `debug`, each outline is also labelled with its area
|
|
82
|
+
* (`a`), width/height ratio (`r`) and the filter that rejected it. Use it to
|
|
83
|
+
* tune the filters above.
|
|
84
|
+
* @default false
|
|
85
|
+
*/
|
|
86
|
+
debug?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Outline (in grey) the objects that were not accepted as a card.
|
|
89
|
+
* The accepted card is always outlined in red.
|
|
90
|
+
* @default true
|
|
91
|
+
*/
|
|
92
|
+
showRejected?: boolean;
|
|
79
93
|
}
|
|
80
94
|
/** Normalized rectangle (0-1). */
|
|
81
95
|
export interface Box {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PermissionState, PluginListenerHandle } from '@capacitor/core';\n\nexport type CameraPosition = 'rear' | 'front';\n\nexport interface StartOptions {\n /**\n * Which camera to use.\n * @default 'rear'\n */\n position?: CameraPosition;\n}\n\nexport interface CaptureOptions {\n /**\n * JPEG quality (0-100).\n * @default 85\n */\n quality?: number;\n}\n\nexport interface CaptureResult {\n /** Base64 encoded JPEG (without the `data:` prefix). */\n base64: string;\n width: number;\n height: number;\n}\n\nexport interface TorchOptions {\n enabled: boolean;\n}\n\nexport interface ZoomOptions {\n /** Zoom ratio, clamped between the camera's min and max. */\n ratio: number;\n}\n\nexport interface PermissionStatus {\n camera: PermissionState;\n}\n\nexport interface DetectionOptions {\n /**\n * Minimum time between two OCR runs, in milliseconds.\n * The red rectangle is updated on every frame regardless.\n * @default 300\n */\n intervalMs?: number;\n /**\n * How many consecutive OCR runs must read the same card before\n * `cardDetected` fires.\n * @default 2\n */\n stableFrames?: number;\n /**\n * Include a JPEG of the detected card in the event.\n * @default false\n */\n includeImage?: boolean;\n /**\n * Expected width / height of an upright card. Only upright cards are\n * detected; a card lying sideways falls outside the tolerance.\n * @default 0.716 (63 x 88 mm)\n */\n cardAspectRatio?: number;\n /**\n * Allowed relative deviation from `cardAspectRatio` (0.15 = ±15%).\n * Increase it if tilted cards are not detected.\n * @default 0.15\n */\n aspectTolerance?: number;\n /**\n * Smallest card size, as a fraction of the camera frame (0-1).\n * @default 0.08\n */\n minCardArea?: number;\n /**\n * Largest card size, as a fraction of the camera frame (0-1).\n * @default 0.9\n */\n maxCardArea?: number;\n /**\n * Only read text when a card-shaped object is visible. When false, the whole\n * frame is read if no card is found.\n * @default true\n */\n requireCard?: boolean;\n}\n\n/** Normalized rectangle (0-1). */\nexport interface Box {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface TextLine {\n text: string;\n /** Position inside the card image. */\n box: Box;\n}\n\nexport interface CardDetectedEvent {\n /** Best guess for the card name (topmost line of text on the card). */\n name?: string;\n /** Collector number, e.g. `025/165`. */\n collectorNumber?: string;\n /** All text read on the card. */\n text: string;\n lines: TextLine[];\n /** Where the card is in the camera frame. Missing if no card outline was found. */\n cardBox?: Box;\n /** Base64 JPEG of the card, when `includeImage` is true. */\n image?: string;\n}\n\nexport interface CardScannerPlugin {\n /**\n * Opens the camera **behind** the WebView. The WebView background is made\n * transparent, so the HTML page must also use transparent backgrounds\n * where the camera should be visible.\n */\n start(options?: StartOptions): Promise<void>;\n\n /** Stops the camera and removes the preview. */\n stop(): Promise<void>;\n\n /** Takes a photo from the running camera. */\n capture(options?: CaptureOptions): Promise<CaptureResult>;\n\n /** Turns the flashlight on/off. */\n setTorch(options: TorchOptions): Promise<void>;\n\n /** Sets the zoom ratio. */\n setZoom(options: ZoomOptions): Promise<void>;\n\n /** Switches between the rear and front camera. */\n flip(): Promise<void>;\n\n /**\n * Starts looking for cards: draws a red rectangle around the card and reads\n * its text, emitting `cardDetected`. Requires `start()` first. Android only.\n */\n startDetection(options?: DetectionOptions): Promise<void>;\n\n /** Stops card detection and hides the rectangle. */\n stopDetection(): Promise<void>;\n\n /** Fired once per card, when its text has been read consistently. */\n addListener(eventName: 'cardDetected', listener: (event: CardDetectedEvent) => void): Promise<PluginListenerHandle>;\n\n removeAllListeners(): Promise<void>;\n\n checkPermissions(): Promise<PermissionStatus>;\n\n requestPermissions(): Promise<PermissionStatus>;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PermissionState, PluginListenerHandle } from '@capacitor/core';\n\nexport type CameraPosition = 'rear' | 'front';\n\nexport interface StartOptions {\n /**\n * Which camera to use.\n * @default 'rear'\n */\n position?: CameraPosition;\n}\n\nexport interface CaptureOptions {\n /**\n * JPEG quality (0-100).\n * @default 85\n */\n quality?: number;\n}\n\nexport interface CaptureResult {\n /** Base64 encoded JPEG (without the `data:` prefix). */\n base64: string;\n width: number;\n height: number;\n}\n\nexport interface TorchOptions {\n enabled: boolean;\n}\n\nexport interface ZoomOptions {\n /** Zoom ratio, clamped between the camera's min and max. */\n ratio: number;\n}\n\nexport interface PermissionStatus {\n camera: PermissionState;\n}\n\nexport interface DetectionOptions {\n /**\n * Minimum time between two OCR runs, in milliseconds.\n * The red rectangle is updated on every frame regardless.\n * @default 300\n */\n intervalMs?: number;\n /**\n * How many consecutive OCR runs must read the same card before\n * `cardDetected` fires.\n * @default 2\n */\n stableFrames?: number;\n /**\n * Include a JPEG of the detected card in the event.\n * @default false\n */\n includeImage?: boolean;\n /**\n * Expected width / height of an upright card. Only upright cards are\n * detected; a card lying sideways falls outside the tolerance.\n * @default 0.716 (63 x 88 mm)\n */\n cardAspectRatio?: number;\n /**\n * Allowed relative deviation from `cardAspectRatio` (0.15 = ±15%).\n * Increase it if tilted cards are not detected.\n * @default 0.15\n */\n aspectTolerance?: number;\n /**\n * Smallest card size, as a fraction of the camera frame (0-1).\n * @default 0.08\n */\n minCardArea?: number;\n /**\n * Largest card size, as a fraction of the camera frame (0-1).\n * @default 0.9\n */\n maxCardArea?: number;\n /**\n * Only read text when a card-shaped object is visible. When false, the whole\n * frame is read if no card is found.\n * @default true\n */\n requireCard?: boolean;\n /**\n * Every object found is always outlined: red when accepted as a card, grey\n * when rejected. With `debug`, each outline is also labelled with its area\n * (`a`), width/height ratio (`r`) and the filter that rejected it. Use it to\n * tune the filters above.\n * @default false\n */\n debug?: boolean;\n /**\n * Outline (in grey) the objects that were not accepted as a card.\n * The accepted card is always outlined in red.\n * @default true\n */\n showRejected?: boolean;\n}\n\n/** Normalized rectangle (0-1). */\nexport interface Box {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport interface TextLine {\n text: string;\n /** Position inside the card image. */\n box: Box;\n}\n\nexport interface CardDetectedEvent {\n /** Best guess for the card name (topmost line of text on the card). */\n name?: string;\n /** Collector number, e.g. `025/165`. */\n collectorNumber?: string;\n /** All text read on the card. */\n text: string;\n lines: TextLine[];\n /** Where the card is in the camera frame. Missing if no card outline was found. */\n cardBox?: Box;\n /** Base64 JPEG of the card, when `includeImage` is true. */\n image?: string;\n}\n\nexport interface CardScannerPlugin {\n /**\n * Opens the camera **behind** the WebView. The WebView background is made\n * transparent, so the HTML page must also use transparent backgrounds\n * where the camera should be visible.\n */\n start(options?: StartOptions): Promise<void>;\n\n /** Stops the camera and removes the preview. */\n stop(): Promise<void>;\n\n /** Takes a photo from the running camera. */\n capture(options?: CaptureOptions): Promise<CaptureResult>;\n\n /** Turns the flashlight on/off. */\n setTorch(options: TorchOptions): Promise<void>;\n\n /** Sets the zoom ratio. */\n setZoom(options: ZoomOptions): Promise<void>;\n\n /** Switches between the rear and front camera. */\n flip(): Promise<void>;\n\n /**\n * Starts looking for cards: draws a red rectangle around the card and reads\n * its text, emitting `cardDetected`. Requires `start()` first. Android only.\n */\n startDetection(options?: DetectionOptions): Promise<void>;\n\n /** Stops card detection and hides the rectangle. */\n stopDetection(): Promise<void>;\n\n /** Fired once per card, when its text has been read consistently. */\n addListener(eventName: 'cardDetected', listener: (event: CardDetectedEvent) => void): Promise<PluginListenerHandle>;\n\n removeAllListeners(): Promise<void>;\n\n checkPermissions(): Promise<PermissionStatus>;\n\n requestPermissions(): Promise<PermissionStatus>;\n}\n"]}
|