capacitor-card-scanner 0.1.0 → 0.3.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 CHANGED
@@ -328,11 +328,17 @@ requestPermissions() => Promise<PermissionStatus>
328
328
 
329
329
  #### DetectionOptions
330
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> |
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> | Draw every object found, labelled with its area (`a`) and width/height ratio (`r`): red when accepted as a card, grey when rejected (with the filter that rejected it). Use it to tune the filters above. | <code>false</code> |
336
342
 
337
343
 
338
344
  #### PluginListenerHandle
@@ -1,249 +1,324 @@
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
- public interface BoxListener {
43
- /**
44
- * @param box card bounds normalized (0-1) to the upright frame, or null when no card is visible
45
- */
46
- void onBox(@Nullable RectF box, int frameWidth, int frameHeight);
47
- }
48
-
49
- // e.g. "025/165", "SV001/SV122", "TG05/TG30", "GG01/GG70"
50
- 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");
51
- private static final Pattern HEADER = Pattern.compile(
52
- "^(basic|stage\\s*\\d|trainer|item|supporter|stadium|energy|pok[eé]mon|evolves|hp\\b)",
53
- Pattern.CASE_INSENSITIVE
54
- );
55
- private static final Pattern HP_SUFFIX = Pattern.compile("\\s*HP\\s*\\d+\\s*$", Pattern.CASE_INSENSITIVE);
56
- private static final Pattern LETTER = Pattern.compile("\\p{L}");
57
- /** After this long without reading a card, the same card can be reported again. */
58
- private static final long RESET_AFTER_MS = 1500;
59
- /** Objects smaller than this fraction of the frame are ignored. */
60
- private static final float MIN_CARD_AREA = 0.08f;
61
- /** Extra margin around the detected card before running OCR. */
62
- private static final float CROP_MARGIN = 0.04f;
63
-
64
- private final ObjectDetector objectDetector = ObjectDetection.getClient(
65
- new ObjectDetectorOptions.Builder().setDetectorMode(ObjectDetectorOptions.STREAM_MODE).build()
66
- );
67
- private final TextRecognizer recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS);
68
- private final long intervalMs;
69
- private final int stableFrames;
70
- private final boolean includeImage;
71
- private final Listener listener;
72
- private volatile BoxListener boxListener;
73
-
74
- // Only touched from the analyzer thread.
75
- private long lastOcrAt;
76
- private long lastSeenAt;
77
- private String candidateKey;
78
- private int candidateCount;
79
- private String lastEmittedKey;
80
-
81
- public CardDetector(long intervalMs, int stableFrames, boolean includeImage, Listener listener) {
82
- this.intervalMs = intervalMs;
83
- this.stableFrames = Math.max(1, stableFrames);
84
- this.includeImage = includeImage;
85
- this.listener = listener;
86
- }
87
-
88
- public void setBoxListener(BoxListener boxListener) {
89
- this.boxListener = boxListener;
90
- }
91
-
92
- public void close() {
93
- objectDetector.close();
94
- recognizer.close();
95
- }
96
-
97
- @Override
98
- public void analyze(@NonNull ImageProxy image) {
99
- long now = SystemClock.elapsedRealtime();
100
- int rotation = image.getImageInfo().getRotationDegrees();
101
- Bitmap raw;
102
- try {
103
- raw = image.toBitmap();
104
- } finally {
105
- image.close();
106
- }
107
- int frameW = rotation % 180 == 0 ? raw.getWidth() : raw.getHeight();
108
- int frameH = rotation % 180 == 0 ? raw.getHeight() : raw.getWidth();
109
-
110
- try {
111
- List<DetectedObject> objects = Tasks.await(objectDetector.process(InputImage.fromBitmap(raw, rotation)));
112
- Rect card = pickCard(objects, frameW, frameH);
113
- RectF cardBox = card == null ? null : new RectF(
114
- card.left / (float) frameW,
115
- card.top / (float) frameH,
116
- card.right / (float) frameW,
117
- card.bottom / (float) frameH
118
- );
119
- BoxListener bl = boxListener;
120
- if (bl != null) bl.onBox(cardBox, frameW, frameH);
121
-
122
- if (now - lastOcrAt < intervalMs) return;
123
- lastOcrAt = now;
124
-
125
- Bitmap upright = rotate(raw, rotation);
126
- Bitmap region = card == null ? upright : crop(upright, card);
127
- Text text = Tasks.await(recognizer.process(InputImage.fromBitmap(region, 0)));
128
- handle(text, region, cardBox, now);
129
- } catch (Exception e) {
130
- Logger.error("CardDetector", "Detection failed", e);
131
- }
132
- }
133
-
134
- /** The biggest detected object, if it is large enough to be a card held close to the camera. */
135
- @Nullable
136
- private static Rect pickCard(List<DetectedObject> objects, int frameW, int frameH) {
137
- Rect best = null;
138
- long bestArea = 0;
139
- for (DetectedObject o : objects) {
140
- Rect r = new Rect(o.getBoundingBox());
141
- if (!r.intersect(0, 0, frameW, frameH)) continue;
142
- long area = (long) r.width() * r.height();
143
- if (area > bestArea) {
144
- best = r;
145
- bestArea = area;
146
- }
147
- }
148
- if (best == null || bestArea < MIN_CARD_AREA * frameW * frameH) return null;
149
- return best;
150
- }
151
-
152
- private static Bitmap rotate(Bitmap bitmap, int rotation) {
153
- if (rotation == 0) return bitmap;
154
- Matrix matrix = new Matrix();
155
- matrix.postRotate(rotation);
156
- return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
157
- }
158
-
159
- private static Bitmap crop(Bitmap bitmap, Rect card) {
160
- int mx = Math.round(card.width() * CROP_MARGIN);
161
- int my = Math.round(card.height() * CROP_MARGIN);
162
- int left = Math.max(0, card.left - mx);
163
- int top = Math.max(0, card.top - my);
164
- int right = Math.min(bitmap.getWidth(), card.right + mx);
165
- int bottom = Math.min(bitmap.getHeight(), card.bottom + my);
166
- return Bitmap.createBitmap(bitmap, left, top, right - left, bottom - top);
167
- }
168
-
169
- private void handle(Text text, Bitmap region, @Nullable RectF cardBox, long now) {
170
- float w = region.getWidth();
171
- float h = region.getHeight();
172
- JSArray lines = new JSArray();
173
- String number = null;
174
- String name = null;
175
- int nameTop = Integer.MAX_VALUE;
176
-
177
- for (Text.TextBlock block : text.getTextBlocks()) {
178
- for (Text.Line line : block.getLines()) {
179
- String s = line.getText().trim();
180
- Rect r = line.getBoundingBox();
181
- if (s.isEmpty() || r == null) continue;
182
-
183
- JSObject item = new JSObject();
184
- item.put("text", s);
185
- item.put("box", box(r.left / w, r.top / h, r.width() / w, r.height() / h));
186
- lines.put(item);
187
-
188
- if (number == null) {
189
- Matcher m = NUMBER.matcher(s);
190
- if (m.find()) number = m.group(1) + "/" + m.group(2);
191
- }
192
- if (r.top < nameTop && isNameCandidate(s)) {
193
- name = HP_SUFFIX.matcher(s).replaceAll("").trim();
194
- nameTop = r.top;
195
- }
196
- }
197
- }
198
-
199
- String key = number != null ? number : name;
200
- if (key == null) {
201
- candidateKey = null;
202
- candidateCount = 0;
203
- if (now - lastSeenAt > RESET_AFTER_MS) lastEmittedKey = null;
204
- return;
205
- }
206
- lastSeenAt = now;
207
-
208
- if (key.equals(candidateKey)) {
209
- candidateCount++;
210
- } else {
211
- candidateKey = key;
212
- candidateCount = 1;
213
- }
214
- if (candidateCount < stableFrames || key.equals(lastEmittedKey)) return;
215
- lastEmittedKey = key;
216
-
217
- JSObject result = new JSObject();
218
- result.put("name", name);
219
- result.put("collectorNumber", number);
220
- result.put("text", text.getText());
221
- result.put("lines", lines);
222
- if (cardBox != null) {
223
- result.put("cardBox", box(cardBox.left, cardBox.top, cardBox.width(), cardBox.height()));
224
- }
225
- if (includeImage) {
226
- ByteArrayOutputStream out = new ByteArrayOutputStream();
227
- region.compress(Bitmap.CompressFormat.JPEG, 80, out);
228
- result.put("image", Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP));
229
- }
230
- listener.onCardDetected(result);
231
- }
232
-
233
- private static JSObject box(float x, float y, float width, float height) {
234
- JSObject box = new JSObject();
235
- box.put("x", x);
236
- box.put("y", y);
237
- box.put("width", width);
238
- box.put("height", height);
239
- return box;
240
- }
241
-
242
- private static boolean isNameCandidate(String s) {
243
- if (HEADER.matcher(s).find()) return false;
244
- Matcher m = LETTER.matcher(s);
245
- int letters = 0;
246
- while (m.find()) letters++;
247
- return letters >= 3;
248
- }
249
- }
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.ArrayList;
28
+ import java.util.Collections;
29
+ import java.util.List;
30
+ import java.util.Locale;
31
+ import java.util.regex.Matcher;
32
+ import java.util.regex.Pattern;
33
+
34
+ /**
35
+ * Locates the card in every camera frame (for the on-screen rectangle) and,
36
+ * every {@code intervalMs}, runs OCR inside it. A card is reported once the
37
+ * same collector number (or name) has been read on several consecutive runs.
38
+ */
39
+ public class CardDetector implements ImageAnalysis.Analyzer {
40
+
41
+ public interface Listener {
42
+ void onCardDetected(JSObject result);
43
+ }
44
+
45
+ /** Tuning knobs, all overridable from JS through startDetection(). */
46
+ public static class Options {
47
+
48
+ /** Minimum time between two OCR runs. */
49
+ public long intervalMs = 300;
50
+ /** Consecutive OCR runs that must agree before a card is reported. */
51
+ public int stableFrames = 2;
52
+ /** Include a JPEG of the card in the event. */
53
+ public boolean includeImage = false;
54
+ /** Expected width / height of an upright card (63 x 88 mm). */
55
+ public float cardAspectRatio = 63f / 88f;
56
+ /** Allowed relative deviation from cardAspectRatio (0.15 = +/-15%). */
57
+ public float aspectTolerance = 0.15f;
58
+ /** Smallest card size, as a fraction of the frame area. */
59
+ public float minCardArea = 0.08f;
60
+ /** Largest card size, as a fraction of the frame area. */
61
+ public float maxCardArea = 0.9f;
62
+ /** Only run OCR when a card-shaped object is visible. */
63
+ public boolean requireCard = true;
64
+ /** Report every detected object (with its area/ratio) so the overlay can show why it was rejected. */
65
+ public boolean debug = false;
66
+ }
67
+
68
+ /** An object found by ML Kit, with the values the card filters were checked against. */
69
+ public static class Candidate {
70
+
71
+ /** Bounds normalized (0-1) to the upright frame. */
72
+ public final RectF box;
73
+ /** Fraction of the frame area. */
74
+ public final float area;
75
+ /** Width / height. */
76
+ public final float ratio;
77
+ /** Null when accepted, otherwise the filter that rejected it ("area" or "ratio"). */
78
+ @Nullable
79
+ public final String rejectedBy;
80
+
81
+ Candidate(RectF box, float area, float ratio, @Nullable String rejectedBy) {
82
+ this.box = box;
83
+ this.area = area;
84
+ this.ratio = ratio;
85
+ this.rejectedBy = rejectedBy;
86
+ }
87
+
88
+ public String label() {
89
+ String s = String.format(Locale.US, "a=%.2f r=%.2f", area, ratio);
90
+ return rejectedBy == null ? s : s + " x" + rejectedBy;
91
+ }
92
+ }
93
+
94
+ public interface BoxListener {
95
+ /**
96
+ * @param card card bounds normalized (0-1) to the upright frame, or null when no card is visible
97
+ * @param candidates every detected object; empty unless debug is on
98
+ */
99
+ void onDetections(@Nullable RectF card, List<Candidate> candidates, int frameWidth, int frameHeight);
100
+ }
101
+
102
+ // e.g. "025/165", "SV001/SV122", "TG05/TG30", "GG01/GG70"
103
+ 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");
104
+ private static final Pattern HEADER = Pattern.compile(
105
+ "^(basic|stage\\s*\\d|trainer|item|supporter|stadium|energy|pok[eé]mon|evolves|hp\\b)",
106
+ Pattern.CASE_INSENSITIVE
107
+ );
108
+ private static final Pattern HP_SUFFIX = Pattern.compile("\\s*HP\\s*\\d+\\s*$", Pattern.CASE_INSENSITIVE);
109
+ private static final Pattern LETTER = Pattern.compile("\\p{L}");
110
+ /** After this long without reading a card, the same card can be reported again. */
111
+ private static final long RESET_AFTER_MS = 1500;
112
+ /** Extra margin around the detected card before running OCR. */
113
+ private static final float CROP_MARGIN = 0.04f;
114
+
115
+ private final ObjectDetector objectDetector = ObjectDetection.getClient(
116
+ new ObjectDetectorOptions.Builder().setDetectorMode(ObjectDetectorOptions.STREAM_MODE).build()
117
+ );
118
+ private final TextRecognizer recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS);
119
+ private final Options options;
120
+ private final Listener listener;
121
+ private volatile BoxListener boxListener;
122
+
123
+ // Only touched from the analyzer thread.
124
+ private long lastOcrAt;
125
+ private long lastSeenAt;
126
+ private String candidateKey;
127
+ private int candidateCount;
128
+ private String lastEmittedKey;
129
+
130
+ public CardDetector(Options options, Listener listener) {
131
+ this.options = options;
132
+ this.options.stableFrames = Math.max(1, options.stableFrames);
133
+ this.listener = listener;
134
+ }
135
+
136
+ public void setBoxListener(BoxListener boxListener) {
137
+ this.boxListener = boxListener;
138
+ }
139
+
140
+ public void close() {
141
+ objectDetector.close();
142
+ recognizer.close();
143
+ }
144
+
145
+ @Override
146
+ public void analyze(@NonNull ImageProxy image) {
147
+ long now = SystemClock.elapsedRealtime();
148
+ Bitmap upright;
149
+ try {
150
+ // Rotate once, so both detectors and all coordinates share the same upright frame.
151
+ upright = rotate(image.toBitmap(), image.getImageInfo().getRotationDegrees());
152
+ } finally {
153
+ image.close();
154
+ }
155
+ int frameW = upright.getWidth();
156
+ int frameH = upright.getHeight();
157
+
158
+ try {
159
+ List<DetectedObject> objects = Tasks.await(objectDetector.process(InputImage.fromBitmap(upright, 0)));
160
+ List<Candidate> candidates = options.debug ? new ArrayList<>() : Collections.emptyList();
161
+ Rect card = pickCard(objects, frameW, frameH, candidates);
162
+ RectF cardBox = card == null ? null : normalize(card, frameW, frameH);
163
+ BoxListener bl = boxListener;
164
+ if (bl != null) bl.onDetections(cardBox, candidates, frameW, frameH);
165
+
166
+ if (now - lastOcrAt < options.intervalMs) return;
167
+ lastOcrAt = now;
168
+
169
+ if (card == null && options.requireCard) {
170
+ noCardRead(now);
171
+ return;
172
+ }
173
+
174
+ Bitmap region = card == null ? upright : crop(upright, card);
175
+ Text text = Tasks.await(recognizer.process(InputImage.fromBitmap(region, 0)));
176
+ handle(text, region, cardBox, now);
177
+ } catch (Exception e) {
178
+ Logger.error("CardDetector", "Detection failed", e);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * The biggest detected object with the size and upright proportions of a card.
184
+ * When debug is on, every object is added to {@code candidates}.
185
+ */
186
+ @Nullable
187
+ private Rect pickCard(List<DetectedObject> objects, int frameW, int frameH, List<Candidate> candidates) {
188
+ float frameArea = (float) frameW * frameH;
189
+ float minRatio = options.cardAspectRatio * (1f - options.aspectTolerance);
190
+ float maxRatio = options.cardAspectRatio * (1f + options.aspectTolerance);
191
+ Rect best = null;
192
+ long bestArea = 0;
193
+ for (DetectedObject o : objects) {
194
+ Rect r = new Rect(o.getBoundingBox());
195
+ if (!r.intersect(0, 0, frameW, frameH) || r.height() == 0) continue;
196
+
197
+ long area = (long) r.width() * r.height();
198
+ float fraction = area / frameArea;
199
+ float ratio = r.width() / (float) r.height();
200
+ String rejectedBy = null;
201
+ if (fraction < options.minCardArea || fraction > options.maxCardArea) {
202
+ rejectedBy = "area";
203
+ } else if (ratio < minRatio || ratio > maxRatio) {
204
+ rejectedBy = "ratio";
205
+ }
206
+ if (options.debug) {
207
+ candidates.add(new Candidate(normalize(r, frameW, frameH), fraction, ratio, rejectedBy));
208
+ }
209
+ if (rejectedBy != null) continue;
210
+
211
+ if (area > bestArea) {
212
+ best = r;
213
+ bestArea = area;
214
+ }
215
+ }
216
+ return best;
217
+ }
218
+
219
+ private static RectF normalize(Rect r, int frameW, int frameH) {
220
+ return new RectF(r.left / (float) frameW, r.top / (float) frameH, r.right / (float) frameW, r.bottom / (float) frameH);
221
+ }
222
+
223
+ private static Bitmap rotate(Bitmap bitmap, int rotation) {
224
+ if (rotation == 0) return bitmap;
225
+ Matrix matrix = new Matrix();
226
+ matrix.postRotate(rotation);
227
+ return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
228
+ }
229
+
230
+ private static Bitmap crop(Bitmap bitmap, Rect card) {
231
+ int mx = Math.round(card.width() * CROP_MARGIN);
232
+ int my = Math.round(card.height() * CROP_MARGIN);
233
+ int left = Math.max(0, card.left - mx);
234
+ int top = Math.max(0, card.top - my);
235
+ int right = Math.min(bitmap.getWidth(), card.right + mx);
236
+ int bottom = Math.min(bitmap.getHeight(), card.bottom + my);
237
+ return Bitmap.createBitmap(bitmap, left, top, right - left, bottom - top);
238
+ }
239
+
240
+ private void handle(Text text, Bitmap region, @Nullable RectF cardBox, long now) {
241
+ float w = region.getWidth();
242
+ float h = region.getHeight();
243
+ JSArray lines = new JSArray();
244
+ String number = null;
245
+ String name = null;
246
+ int nameTop = Integer.MAX_VALUE;
247
+
248
+ for (Text.TextBlock block : text.getTextBlocks()) {
249
+ for (Text.Line line : block.getLines()) {
250
+ String s = line.getText().trim();
251
+ Rect r = line.getBoundingBox();
252
+ if (s.isEmpty() || r == null) continue;
253
+
254
+ JSObject item = new JSObject();
255
+ item.put("text", s);
256
+ item.put("box", box(r.left / w, r.top / h, r.width() / w, r.height() / h));
257
+ lines.put(item);
258
+
259
+ if (number == null) {
260
+ Matcher m = NUMBER.matcher(s);
261
+ if (m.find()) number = m.group(1) + "/" + m.group(2);
262
+ }
263
+ if (r.top < nameTop && isNameCandidate(s)) {
264
+ name = HP_SUFFIX.matcher(s).replaceAll("").trim();
265
+ nameTop = r.top;
266
+ }
267
+ }
268
+ }
269
+
270
+ String key = number != null ? number : name;
271
+ if (key == null) {
272
+ noCardRead(now);
273
+ return;
274
+ }
275
+ lastSeenAt = now;
276
+
277
+ if (key.equals(candidateKey)) {
278
+ candidateCount++;
279
+ } else {
280
+ candidateKey = key;
281
+ candidateCount = 1;
282
+ }
283
+ if (candidateCount < options.stableFrames || key.equals(lastEmittedKey)) return;
284
+ lastEmittedKey = key;
285
+
286
+ JSObject result = new JSObject();
287
+ result.put("name", name);
288
+ result.put("collectorNumber", number);
289
+ result.put("text", text.getText());
290
+ result.put("lines", lines);
291
+ if (cardBox != null) {
292
+ result.put("cardBox", box(cardBox.left, cardBox.top, cardBox.width(), cardBox.height()));
293
+ }
294
+ if (options.includeImage) {
295
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
296
+ region.compress(Bitmap.CompressFormat.JPEG, 80, out);
297
+ result.put("image", Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP));
298
+ }
299
+ listener.onCardDetected(result);
300
+ }
301
+
302
+ private void noCardRead(long now) {
303
+ candidateKey = null;
304
+ candidateCount = 0;
305
+ if (now - lastSeenAt > RESET_AFTER_MS) lastEmittedKey = null;
306
+ }
307
+
308
+ private static JSObject box(float x, float y, float width, float height) {
309
+ JSObject box = new JSObject();
310
+ box.put("x", x);
311
+ box.put("y", y);
312
+ box.put("width", width);
313
+ box.put("height", height);
314
+ return box;
315
+ }
316
+
317
+ private static boolean isNameCandidate(String s) {
318
+ if (HEADER.matcher(s).find()) return false;
319
+ Matcher m = LETTER.matcher(s);
320
+ int letters = 0;
321
+ while (m.find()) letters++;
322
+ return letters >= 3;
323
+ }
324
+ }
@@ -151,10 +151,10 @@ public class CardScanner {
151
151
  closeDetector();
152
152
  detector = newDetector;
153
153
  DetectionOverlay target = overlay;
154
- detector.setBoxListener((box, frameW, frameH) ->
154
+ detector.setBoxListener((card, candidates, frameW, frameH) ->
155
155
  target.post(() -> {
156
156
  // Ignore frames that arrive after detection was stopped or restarted.
157
- if (detector == newDetector) target.setBox(box, frameW, frameH);
157
+ if (detector == newDetector) target.setDetections(card, candidates, frameW, frameH);
158
158
  })
159
159
  );
160
160
  bind();
@@ -1,159 +1,165 @@
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 detector = new CardDetector(
95
- call.getInt("intervalMs", 300),
96
- call.getInt("stableFrames", 2),
97
- Boolean.TRUE.equals(call.getBoolean("includeImage", false)),
98
- result -> notifyListeners("cardDetected", result)
99
- );
100
- getActivity().runOnUiThread(() -> {
101
- try {
102
- implementation.startDetection(detector);
103
- call.resolve();
104
- } catch (Exception e) {
105
- detector.close();
106
- call.reject("Failed to start detection: " + e.getMessage(), e);
107
- }
108
- });
109
- }
110
-
111
- @PluginMethod
112
- public void stopDetection(PluginCall call) {
113
- getActivity().runOnUiThread(() -> {
114
- implementation.stopDetection();
115
- call.resolve();
116
- });
117
- }
118
-
119
- @PluginMethod
120
- public void setTorch(PluginCall call) {
121
- if (!ensureRunning(call)) return;
122
- implementation.setTorch(Boolean.TRUE.equals(call.getBoolean("enabled", false)));
123
- call.resolve();
124
- }
125
-
126
- @PluginMethod
127
- public void setZoom(PluginCall call) {
128
- if (!ensureRunning(call)) return;
129
- Float ratio = call.getFloat("ratio");
130
- if (ratio == null) {
131
- call.reject("Missing 'ratio'");
132
- return;
133
- }
134
- implementation.setZoom(ratio);
135
- call.resolve();
136
- }
137
-
138
- private boolean ensureRunning(PluginCall call) {
139
- if (!implementation.isRunning()) {
140
- call.reject("Camera is not running", "NOT_RUNNING");
141
- return false;
142
- }
143
- return true;
144
- }
145
-
146
- private CardScanner.Callback<Void> voidCallback(PluginCall call) {
147
- return new CardScanner.Callback<>() {
148
- @Override
149
- public void onSuccess(Void result) {
150
- call.resolve();
151
- }
152
-
153
- @Override
154
- public void onError(String message, Exception e) {
155
- call.reject(message, e);
156
- }
157
- };
158
- }
159
- }
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
+
105
+ CardDetector detector = new CardDetector(options, result -> notifyListeners("cardDetected", result));
106
+ getActivity().runOnUiThread(() -> {
107
+ try {
108
+ implementation.startDetection(detector);
109
+ call.resolve();
110
+ } catch (Exception e) {
111
+ detector.close();
112
+ call.reject("Failed to start detection: " + e.getMessage(), e);
113
+ }
114
+ });
115
+ }
116
+
117
+ @PluginMethod
118
+ public void stopDetection(PluginCall call) {
119
+ getActivity().runOnUiThread(() -> {
120
+ implementation.stopDetection();
121
+ call.resolve();
122
+ });
123
+ }
124
+
125
+ @PluginMethod
126
+ public void setTorch(PluginCall call) {
127
+ if (!ensureRunning(call)) return;
128
+ implementation.setTorch(Boolean.TRUE.equals(call.getBoolean("enabled", false)));
129
+ call.resolve();
130
+ }
131
+
132
+ @PluginMethod
133
+ public void setZoom(PluginCall call) {
134
+ if (!ensureRunning(call)) return;
135
+ Float ratio = call.getFloat("ratio");
136
+ if (ratio == null) {
137
+ call.reject("Missing 'ratio'");
138
+ return;
139
+ }
140
+ implementation.setZoom(ratio);
141
+ call.resolve();
142
+ }
143
+
144
+ private boolean ensureRunning(PluginCall call) {
145
+ if (!implementation.isRunning()) {
146
+ call.reject("Camera is not running", "NOT_RUNNING");
147
+ return false;
148
+ }
149
+ return true;
150
+ }
151
+
152
+ private CardScanner.Callback<Void> voidCallback(PluginCall call) {
153
+ return new CardScanner.Callback<>() {
154
+ @Override
155
+ public void onSuccess(Void result) {
156
+ call.resolve();
157
+ }
158
+
159
+ @Override
160
+ public void onError(String message, Exception e) {
161
+ call.reject(message, e);
162
+ }
163
+ };
164
+ }
165
+ }
@@ -8,35 +8,52 @@ import android.graphics.RectF;
8
8
  import android.os.SystemClock;
9
9
  import android.view.View;
10
10
  import androidx.annotation.Nullable;
11
+ import java.util.Collections;
12
+ import java.util.List;
11
13
 
12
14
  /**
13
15
  * Draws a red rectangle around the detected card. Sits between the camera
14
16
  * preview and the (transparent) WebView, and maps frame coordinates the same
15
- * way PreviewView's FILL_CENTER does.
17
+ * way PreviewView's FILL_CENTER does. In debug mode it also draws every object
18
+ * ML Kit found, labelled with its area/ratio (grey when rejected).
16
19
  */
17
20
  public class DetectionOverlay extends View {
18
21
 
19
22
  /** Keep the rectangle this long after the card was last seen, to avoid flicker. */
20
23
  private static final long HIDE_AFTER_MS = 300;
21
24
 
22
- private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
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);
23
28
  private final float radius;
29
+ private final float labelPadding;
24
30
  private final RectF drawRect = new RectF();
25
31
 
26
32
  @Nullable
27
- private RectF box;
33
+ private RectF card;
34
+ private List<CardDetector.Candidate> candidates = Collections.emptyList();
28
35
  private int frameWidth;
29
36
  private int frameHeight;
30
37
  private boolean mirror;
31
- private long lastBoxAt;
38
+ private long lastCardAt;
32
39
 
33
40
  public DetectionOverlay(Context context) {
34
41
  super(context);
35
42
  float density = context.getResources().getDisplayMetrics().density;
36
43
  radius = 8 * density;
37
- paint.setColor(Color.RED);
38
- paint.setStyle(Paint.Style.STROKE);
39
- paint.setStrokeWidth(3 * density);
44
+ labelPadding = 4 * density;
45
+
46
+ cardPaint.setColor(Color.RED);
47
+ cardPaint.setStyle(Paint.Style.STROKE);
48
+ cardPaint.setStrokeWidth(3 * density);
49
+
50
+ rejectedPaint.setColor(Color.argb(200, 180, 180, 180));
51
+ rejectedPaint.setStyle(Paint.Style.STROKE);
52
+ rejectedPaint.setStrokeWidth(2 * density);
53
+
54
+ labelPaint.setColor(Color.WHITE);
55
+ labelPaint.setTextSize(13 * density);
56
+ labelPaint.setShadowLayer(3 * density, 0, 0, Color.BLACK);
40
57
  }
41
58
 
42
59
  public void setMirror(boolean mirror) {
@@ -44,28 +61,44 @@ public class DetectionOverlay extends View {
44
61
  }
45
62
 
46
63
  /** Must be called on the UI thread. */
47
- public void setBox(@Nullable RectF box, int frameWidth, int frameHeight) {
64
+ public void setDetections(@Nullable RectF card, List<CardDetector.Candidate> candidates, int frameWidth, int frameHeight) {
48
65
  long now = SystemClock.uptimeMillis();
49
- if (box == null) {
50
- if (this.box == null || now - lastBoxAt < HIDE_AFTER_MS) return;
51
- } else {
52
- lastBoxAt = now;
66
+ if (card != null) {
67
+ lastCardAt = now;
68
+ this.card = card;
69
+ } else if (now - lastCardAt >= HIDE_AFTER_MS) {
70
+ this.card = null;
53
71
  }
54
- this.box = box;
72
+ this.candidates = candidates;
55
73
  this.frameWidth = frameWidth;
56
74
  this.frameHeight = frameHeight;
57
75
  invalidate();
58
76
  }
59
77
 
60
78
  public void clear() {
61
- box = null;
79
+ card = null;
80
+ candidates = Collections.emptyList();
62
81
  invalidate();
63
82
  }
64
83
 
65
84
  @Override
66
85
  protected void onDraw(Canvas canvas) {
67
- if (box == null || frameWidth == 0 || frameHeight == 0) return;
86
+ if (frameWidth == 0 || frameHeight == 0) return;
87
+
88
+ for (CardDetector.Candidate c : candidates) {
89
+ map(c.box);
90
+ canvas.drawRoundRect(drawRect, radius, radius, c.rejectedBy == null ? cardPaint : rejectedPaint);
91
+ canvas.drawText(c.label(), drawRect.left + labelPadding, drawRect.top - labelPadding, labelPaint);
92
+ }
93
+
94
+ if (card != null) {
95
+ map(card);
96
+ canvas.drawRoundRect(drawRect, radius, radius, cardPaint);
97
+ }
98
+ }
68
99
 
100
+ /** Maps a normalized frame rectangle into drawRect, in view coordinates. */
101
+ private void map(RectF box) {
69
102
  float vw = getWidth();
70
103
  float vh = getHeight();
71
104
  float scale = Math.max(vw / frameWidth, vh / frameHeight);
@@ -77,6 +110,5 @@ public class DetectionOverlay extends View {
77
110
  float left = mirror ? 1f - box.right : box.left;
78
111
  float right = mirror ? 1f - box.left : box.right;
79
112
  drawRect.set(dx + left * sw, dy + box.top * sh, dx + right * sw, dy + box.bottom * sh);
80
- canvas.drawRoundRect(drawRect, radius, radius, paint);
81
113
  }
82
114
  }
package/dist/docs.json CHANGED
@@ -336,6 +336,78 @@
336
336
  "docs": "Include a JPEG of the detected card in the event.",
337
337
  "complexTypes": [],
338
338
  "type": "boolean | undefined"
339
+ },
340
+ {
341
+ "name": "cardAspectRatio",
342
+ "tags": [
343
+ {
344
+ "text": "0.716 (63 x 88 mm)",
345
+ "name": "default"
346
+ }
347
+ ],
348
+ "docs": "Expected width / height of an upright card. Only upright cards are\ndetected; a card lying sideways falls outside the tolerance.",
349
+ "complexTypes": [],
350
+ "type": "number | undefined"
351
+ },
352
+ {
353
+ "name": "aspectTolerance",
354
+ "tags": [
355
+ {
356
+ "text": "0.15",
357
+ "name": "default"
358
+ }
359
+ ],
360
+ "docs": "Allowed relative deviation from `cardAspectRatio` (0.15 = ±15%).\nIncrease it if tilted cards are not detected.",
361
+ "complexTypes": [],
362
+ "type": "number | undefined"
363
+ },
364
+ {
365
+ "name": "minCardArea",
366
+ "tags": [
367
+ {
368
+ "text": "0.08",
369
+ "name": "default"
370
+ }
371
+ ],
372
+ "docs": "Smallest card size, as a fraction of the camera frame (0-1).",
373
+ "complexTypes": [],
374
+ "type": "number | undefined"
375
+ },
376
+ {
377
+ "name": "maxCardArea",
378
+ "tags": [
379
+ {
380
+ "text": "0.9",
381
+ "name": "default"
382
+ }
383
+ ],
384
+ "docs": "Largest card size, as a fraction of the camera frame (0-1).",
385
+ "complexTypes": [],
386
+ "type": "number | undefined"
387
+ },
388
+ {
389
+ "name": "requireCard",
390
+ "tags": [
391
+ {
392
+ "text": "true",
393
+ "name": "default"
394
+ }
395
+ ],
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
+ "complexTypes": [],
398
+ "type": "boolean | undefined"
399
+ },
400
+ {
401
+ "name": "debug",
402
+ "tags": [
403
+ {
404
+ "text": "false",
405
+ "name": "default"
406
+ }
407
+ ],
408
+ "docs": "Draw every object found, labelled with its area (`a`) and width/height\nratio (`r`): red when accepted as a card, grey when rejected (with the\nfilter that rejected it). Use it to tune the filters above.",
409
+ "complexTypes": [],
410
+ "type": "boolean | undefined"
339
411
  }
340
412
  ]
341
413
  },
@@ -48,6 +48,41 @@ export interface DetectionOptions {
48
48
  * @default false
49
49
  */
50
50
  includeImage?: boolean;
51
+ /**
52
+ * Expected width / height of an upright card. Only upright cards are
53
+ * detected; a card lying sideways falls outside the tolerance.
54
+ * @default 0.716 (63 x 88 mm)
55
+ */
56
+ cardAspectRatio?: number;
57
+ /**
58
+ * Allowed relative deviation from `cardAspectRatio` (0.15 = ±15%).
59
+ * Increase it if tilted cards are not detected.
60
+ * @default 0.15
61
+ */
62
+ aspectTolerance?: number;
63
+ /**
64
+ * Smallest card size, as a fraction of the camera frame (0-1).
65
+ * @default 0.08
66
+ */
67
+ minCardArea?: number;
68
+ /**
69
+ * Largest card size, as a fraction of the camera frame (0-1).
70
+ * @default 0.9
71
+ */
72
+ maxCardArea?: number;
73
+ /**
74
+ * Only read text when a card-shaped object is visible. When false, the whole
75
+ * frame is read if no card is found.
76
+ * @default true
77
+ */
78
+ requireCard?: boolean;
79
+ /**
80
+ * Draw every object found, labelled with its area (`a`) and width/height
81
+ * ratio (`r`): red when accepted as a card, grey when rejected (with the
82
+ * filter that rejected it). Use it to tune the filters above.
83
+ * @default false
84
+ */
85
+ debug?: boolean;
51
86
  }
52
87
  /** Normalized rectangle (0-1). */
53
88
  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\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 * Draw every object found, labelled with its area (`a`) and width/height\n * ratio (`r`): red when accepted as a card, grey when rejected (with the\n * filter that rejected it). Use it to tune the filters above.\n * @default false\n */\n debug?: 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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-card-scanner",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Recognize a pokemon card with camera open in background",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",