capacitor-card-scanner 0.3.1 → 0.3.3

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,18 +328,22 @@ 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> |
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> |
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> |
343
+ | **`repeatAfterMs`** | <code>number</code> | How long (ms) the card must be out of view before the same card can be reported again. While a card stays in view it is reported only once. | <code>1500</code> |
344
+ | **`nameLines`** | <code>number</code> | The card name is looked for only in the first N lines of text on the card, top to bottom. | <code>3</code> |
345
+ | **`reservedWords`** | <code>string[]</code> | Words/phrases that are never the card name (card types, subtypes...). A line made only of these words is skipped, so "Trainer" or "Stage 1" are ignored but "Energy Retrieval" is still a name. Replaces the defaults. | <code>['Basic', 'Stage 1', 'Stage 2', 'Trainer', 'Item', 'Supporter', 'Stadium', 'Energy', 'Special Energy', 'Basic Energy', 'Pokémon', 'Pokémon Tool']</code> |
346
+ | **`reservedPrefixes`** | <code>string[]</code> | Lines starting with any of these are never the card name. Replaces the defaults. | <code>['Evolves from', 'HP']</code> |
343
347
 
344
348
 
345
349
  #### PluginListenerHandle
@@ -24,7 +24,10 @@ 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.text.Normalizer;
27
28
  import java.util.ArrayList;
29
+ import java.util.Arrays;
30
+ import java.util.Comparator;
28
31
  import java.util.List;
29
32
  import java.util.Locale;
30
33
  import java.util.regex.Matcher;
@@ -64,6 +67,20 @@ public class CardDetector implements ImageAnalysis.Analyzer {
64
67
  public boolean debug = false;
65
68
  /** Outline (in grey) the objects that were not accepted as a card. */
66
69
  public boolean showRejected = true;
70
+ /** How long the card must be out of view before the same card can be reported again. */
71
+ public long repeatAfterMs = 1500;
72
+ /** The name is looked for only in the first N lines of the card (top to bottom). */
73
+ public int nameLines = 3;
74
+ /**
75
+ * Words/phrases that are never the card name (card types, subtypes...).
76
+ * A line made only of these words is skipped; "Energy Retrieval" is still a name.
77
+ */
78
+ public List<String> reservedWords = Arrays.asList(
79
+ "Basic", "Stage 1", "Stage 2", "Trainer", "Item", "Supporter", "Stadium",
80
+ "Energy", "Special Energy", "Basic Energy", "Pokémon", "Pokémon Tool"
81
+ );
82
+ /** Lines starting with any of these are never the card name. */
83
+ public List<String> reservedPrefixes = Arrays.asList("Evolves from", "HP");
67
84
  }
68
85
 
69
86
  /** An object found by ML Kit, with the values the card filters were checked against. */
@@ -102,14 +119,10 @@ public class CardDetector implements ImageAnalysis.Analyzer {
102
119
 
103
120
  // e.g. "025/165", "SV001/SV122", "TG05/TG30", "GG01/GG70"
104
121
  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");
105
- private static final Pattern HEADER = Pattern.compile(
106
- "^(basic|stage\\s*\\d|trainer|item|supporter|stadium|energy|pok[eé]mon|evolves|hp\\b)",
107
- Pattern.CASE_INSENSITIVE
108
- );
109
122
  private static final Pattern HP_SUFFIX = Pattern.compile("\\s*HP\\s*\\d+\\s*$", Pattern.CASE_INSENSITIVE);
110
123
  private static final Pattern LETTER = Pattern.compile("\\p{L}");
111
- /** After this long without reading a card, the same card can be reported again. */
112
- private static final long RESET_AFTER_MS = 1500;
124
+ /** Names at least this similar are treated as the same card. */
125
+ private static final float SAME_NAME_SIMILARITY = 0.8f;
113
126
  /** Extra margin around the detected card before running OCR. */
114
127
  private static final float CROP_MARGIN = 0.04f;
115
128
 
@@ -119,6 +132,9 @@ public class CardDetector implements ImageAnalysis.Analyzer {
119
132
  private final TextRecognizer recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS);
120
133
  private final Options options;
121
134
  private final Listener listener;
135
+ /** reservedWords / reservedPrefixes, normalized; words longest first. */
136
+ private final List<String> reservedWords = new ArrayList<>();
137
+ private final List<String> reservedPrefixes = new ArrayList<>();
122
138
  private volatile BoxListener boxListener;
123
139
 
124
140
  // Only touched from the analyzer thread.
@@ -126,12 +142,26 @@ public class CardDetector implements ImageAnalysis.Analyzer {
126
142
  private long lastSeenAt;
127
143
  private String candidateKey;
128
144
  private int candidateCount;
129
- private String lastEmittedKey;
145
+ // Last reported card. A reading is the same card if either its name or its
146
+ // number matches, because OCR does not read the number on every frame.
147
+ private String lastEmittedName;
148
+ private String lastEmittedNumber;
130
149
 
131
150
  public CardDetector(Options options, Listener listener) {
132
151
  this.options = options;
133
152
  this.options.stableFrames = Math.max(1, options.stableFrames);
153
+ this.options.nameLines = Math.max(1, options.nameLines);
134
154
  this.listener = listener;
155
+ for (String w : options.reservedWords) {
156
+ String n = normalize(w);
157
+ if (!n.isEmpty()) reservedWords.add(n);
158
+ }
159
+ // Longest first, so "Pokémon Tool" is removed before "Pokémon".
160
+ reservedWords.sort(Comparator.comparingInt(String::length).reversed());
161
+ for (String p : options.reservedPrefixes) {
162
+ String n = normalize(p);
163
+ if (!n.isEmpty()) reservedPrefixes.add(n);
164
+ }
135
165
  }
136
166
 
137
167
  public boolean isDebug() {
@@ -169,6 +199,9 @@ public class CardDetector implements ImageAnalysis.Analyzer {
169
199
  List<Candidate> candidates = new ArrayList<>();
170
200
  Rect card = pickCard(objects, frameW, frameH, candidates);
171
201
  RectF cardBox = card == null ? null : normalize(card, frameW, frameH);
202
+ // The card counts as "in view" while its outline is visible, even if
203
+ // its text is not readable on this frame.
204
+ if (card != null) lastSeenAt = now;
172
205
  BoxListener bl = boxListener;
173
206
  if (bl != null) bl.onDetections(cardBox, candidates, frameW, frameH);
174
207
 
@@ -249,8 +282,7 @@ public class CardDetector implements ImageAnalysis.Analyzer {
249
282
  float h = region.getHeight();
250
283
  JSArray lines = new JSArray();
251
284
  String number = null;
252
- String name = null;
253
- int nameTop = Integer.MAX_VALUE;
285
+ List<Text.Line> ordered = new ArrayList<>();
254
286
 
255
287
  for (Text.TextBlock block : text.getTextBlocks()) {
256
288
  for (Text.Line line : block.getLines()) {
@@ -267,12 +299,10 @@ public class CardDetector implements ImageAnalysis.Analyzer {
267
299
  Matcher m = NUMBER.matcher(s);
268
300
  if (m.find()) number = m.group(1) + "/" + m.group(2);
269
301
  }
270
- if (r.top < nameTop && isNameCandidate(s)) {
271
- name = HP_SUFFIX.matcher(s).replaceAll("").trim();
272
- nameTop = r.top;
273
- }
302
+ ordered.add(line);
274
303
  }
275
304
  }
305
+ String name = findName(ordered);
276
306
 
277
307
  String key = number != null ? number : name;
278
308
  if (key == null) {
@@ -287,8 +317,9 @@ public class CardDetector implements ImageAnalysis.Analyzer {
287
317
  candidateKey = key;
288
318
  candidateCount = 1;
289
319
  }
290
- if (candidateCount < options.stableFrames || key.equals(lastEmittedKey)) return;
291
- lastEmittedKey = key;
320
+ if (candidateCount < options.stableFrames || isLastEmitted(name, number)) return;
321
+ lastEmittedName = name;
322
+ lastEmittedNumber = number;
292
323
 
293
324
  JSObject result = new JSObject();
294
325
  result.put("name", name);
@@ -309,7 +340,33 @@ public class CardDetector implements ImageAnalysis.Analyzer {
309
340
  private void noCardRead(long now) {
310
341
  candidateKey = null;
311
342
  candidateCount = 0;
312
- if (now - lastSeenAt > RESET_AFTER_MS) lastEmittedKey = null;
343
+ if (now - lastSeenAt > options.repeatAfterMs) {
344
+ lastEmittedName = null;
345
+ lastEmittedNumber = null;
346
+ }
347
+ }
348
+
349
+ private boolean isLastEmitted(String name, String number) {
350
+ if (number != null && number.equals(lastEmittedNumber)) return true;
351
+ return name != null && lastEmittedName != null && similarity(name, lastEmittedName) >= SAME_NAME_SIMILARITY;
352
+ }
353
+
354
+ /** 0-1 similarity (LCS based), so an OCR slip like "Evolutlon" still matches "Evolution". */
355
+ private static float similarity(String a, String b) {
356
+ a = a.toLowerCase(Locale.ROOT);
357
+ b = b.toLowerCase(Locale.ROOT);
358
+ if (a.equals(b)) return 1f;
359
+ int[] prev = new int[b.length() + 1];
360
+ int[] cur = new int[b.length() + 1];
361
+ for (int i = 1; i <= a.length(); i++) {
362
+ for (int j = 1; j <= b.length(); j++) {
363
+ cur[j] = a.charAt(i - 1) == b.charAt(j - 1) ? prev[j - 1] + 1 : Math.max(prev[j], cur[j - 1]);
364
+ }
365
+ int[] tmp = prev;
366
+ prev = cur;
367
+ cur = tmp;
368
+ }
369
+ return 2f * prev[b.length()] / (a.length() + b.length());
313
370
  }
314
371
 
315
372
  private static JSObject box(float x, float y, float width, float height) {
@@ -321,11 +378,41 @@ public class CardDetector implements ImageAnalysis.Analyzer {
321
378
  return box;
322
379
  }
323
380
 
324
- private static boolean isNameCandidate(String s) {
325
- if (HEADER.matcher(s).find()) return false;
381
+ /** First line, among the top {@code nameLines}, that is not made of reserved words. */
382
+ @Nullable
383
+ private String findName(List<Text.Line> lines) {
384
+ lines.sort(Comparator.comparingInt(l -> l.getBoundingBox().top));
385
+ for (int i = 0; i < lines.size() && i < options.nameLines; i++) {
386
+ String s = HP_SUFFIX.matcher(lines.get(i).getText().trim()).replaceAll("").trim();
387
+ if (!isReserved(s) && countLetters(s) >= 3) return s;
388
+ }
389
+ return null;
390
+ }
391
+
392
+ private boolean isReserved(String line) {
393
+ String rest = " " + normalize(line) + " ";
394
+ for (String w : reservedWords) {
395
+ rest = rest.replace(" " + w + " ", " ");
396
+ }
397
+ rest = rest.trim();
398
+ if (countLetters(rest) < 3) return true;
399
+ // Checked after removing reserved words, so "STAGE 1 Evolves from Pichu" is caught too.
400
+ for (String p : reservedPrefixes) {
401
+ if (rest.equals(p) || rest.startsWith(p + " ")) return true;
402
+ }
403
+ return false;
404
+ }
405
+
406
+ /** Upper case, no accents, only letters/digits separated by single spaces. */
407
+ private static String normalize(String s) {
408
+ String n = Normalizer.normalize(s, Normalizer.Form.NFD).replaceAll("\\p{M}", "");
409
+ return n.toUpperCase(Locale.ROOT).replaceAll("[^\\p{L}\\p{Nd}]+", " ").trim();
410
+ }
411
+
412
+ private static int countLetters(String s) {
326
413
  Matcher m = LETTER.matcher(s);
327
414
  int letters = 0;
328
415
  while (m.find()) letters++;
329
- return letters >= 3;
416
+ return letters;
330
417
  }
331
418
  }
@@ -1,6 +1,8 @@
1
1
  package pt.tcgclassifier.cardscanner;
2
2
 
3
3
  import android.Manifest;
4
+ import androidx.annotation.Nullable;
5
+ import com.getcapacitor.JSArray;
4
6
  import com.getcapacitor.JSObject;
5
7
  import com.getcapacitor.PermissionState;
6
8
  import com.getcapacitor.Plugin;
@@ -9,6 +11,8 @@ import com.getcapacitor.PluginMethod;
9
11
  import com.getcapacitor.annotation.CapacitorPlugin;
10
12
  import com.getcapacitor.annotation.Permission;
11
13
  import com.getcapacitor.annotation.PermissionCallback;
14
+ import java.util.ArrayList;
15
+ import java.util.List;
12
16
 
13
17
  @CapacitorPlugin(name = "CardScanner", permissions = { @Permission(strings = { Manifest.permission.CAMERA }, alias = CardScannerPlugin.CAMERA) })
14
18
  public class CardScannerPlugin extends Plugin {
@@ -102,6 +106,12 @@ public class CardScannerPlugin extends Plugin {
102
106
  options.requireCard = call.getBoolean("requireCard", options.requireCard);
103
107
  options.debug = call.getBoolean("debug", options.debug);
104
108
  options.showRejected = call.getBoolean("showRejected", options.showRejected);
109
+ options.repeatAfterMs = call.getInt("repeatAfterMs", (int) options.repeatAfterMs);
110
+ options.nameLines = call.getInt("nameLines", options.nameLines);
111
+ List<String> reservedWords = stringList(call, "reservedWords");
112
+ if (reservedWords != null) options.reservedWords = reservedWords;
113
+ List<String> reservedPrefixes = stringList(call, "reservedPrefixes");
114
+ if (reservedPrefixes != null) options.reservedPrefixes = reservedPrefixes;
105
115
 
106
116
  CardDetector detector = new CardDetector(options, result -> notifyListeners("cardDetected", result));
107
117
  getActivity().runOnUiThread(() -> {
@@ -142,6 +152,19 @@ public class CardScannerPlugin extends Plugin {
142
152
  call.resolve();
143
153
  }
144
154
 
155
+ /** A string array option, or null when it was not passed. */
156
+ @Nullable
157
+ private static List<String> stringList(PluginCall call, String key) {
158
+ JSArray array = call.getArray(key, null);
159
+ if (array == null) return null;
160
+ List<String> list = new ArrayList<>();
161
+ for (int i = 0; i < array.length(); i++) {
162
+ String s = array.optString(i, null);
163
+ if (s != null) list.add(s);
164
+ }
165
+ return list;
166
+ }
167
+
145
168
  private boolean ensureRunning(PluginCall call) {
146
169
  if (!implementation.isRunning()) {
147
170
  call.reject("Camera is not running", "NOT_RUNNING");
package/dist/docs.json CHANGED
@@ -420,6 +420,54 @@
420
420
  "docs": "Outline (in grey) the objects that were not accepted as a card.\nThe accepted card is always outlined in red.",
421
421
  "complexTypes": [],
422
422
  "type": "boolean | undefined"
423
+ },
424
+ {
425
+ "name": "repeatAfterMs",
426
+ "tags": [
427
+ {
428
+ "text": "1500",
429
+ "name": "default"
430
+ }
431
+ ],
432
+ "docs": "How long (ms) the card must be out of view before the same card can be\nreported again. While a card stays in view it is reported only once.",
433
+ "complexTypes": [],
434
+ "type": "number | undefined"
435
+ },
436
+ {
437
+ "name": "nameLines",
438
+ "tags": [
439
+ {
440
+ "text": "3",
441
+ "name": "default"
442
+ }
443
+ ],
444
+ "docs": "The card name is looked for only in the first N lines of text on the\ncard, top to bottom.",
445
+ "complexTypes": [],
446
+ "type": "number | undefined"
447
+ },
448
+ {
449
+ "name": "reservedWords",
450
+ "tags": [
451
+ {
452
+ "text": "['Basic', 'Stage 1', 'Stage 2', 'Trainer', 'Item', 'Supporter', 'Stadium', 'Energy', 'Special Energy', 'Basic Energy', 'Pokémon', 'Pokémon Tool']",
453
+ "name": "default"
454
+ }
455
+ ],
456
+ "docs": "Words/phrases that are never the card name (card types, subtypes...).\nA line made only of these words is skipped, so \"Trainer\" or \"Stage 1\" are\nignored but \"Energy Retrieval\" is still a name. Replaces the defaults.",
457
+ "complexTypes": [],
458
+ "type": "string[] | undefined"
459
+ },
460
+ {
461
+ "name": "reservedPrefixes",
462
+ "tags": [
463
+ {
464
+ "text": "['Evolves from', 'HP']",
465
+ "name": "default"
466
+ }
467
+ ],
468
+ "docs": "Lines starting with any of these are never the card name.\nReplaces the defaults.",
469
+ "complexTypes": [],
470
+ "type": "string[] | undefined"
423
471
  }
424
472
  ]
425
473
  },
@@ -90,6 +90,31 @@ export interface DetectionOptions {
90
90
  * @default true
91
91
  */
92
92
  showRejected?: boolean;
93
+ /**
94
+ * How long (ms) the card must be out of view before the same card can be
95
+ * reported again. While a card stays in view it is reported only once.
96
+ * @default 1500
97
+ */
98
+ repeatAfterMs?: number;
99
+ /**
100
+ * The card name is looked for only in the first N lines of text on the
101
+ * card, top to bottom.
102
+ * @default 3
103
+ */
104
+ nameLines?: number;
105
+ /**
106
+ * Words/phrases that are never the card name (card types, subtypes...).
107
+ * A line made only of these words is skipped, so "Trainer" or "Stage 1" are
108
+ * ignored but "Energy Retrieval" is still a name. Replaces the defaults.
109
+ * @default ['Basic', 'Stage 1', 'Stage 2', 'Trainer', 'Item', 'Supporter', 'Stadium', 'Energy', 'Special Energy', 'Basic Energy', 'Pokémon', 'Pokémon Tool']
110
+ */
111
+ reservedWords?: string[];
112
+ /**
113
+ * Lines starting with any of these are never the card name.
114
+ * Replaces the defaults.
115
+ * @default ['Evolves from', 'HP']
116
+ */
117
+ reservedPrefixes?: string[];
93
118
  }
94
119
  /** Normalized rectangle (0-1). */
95
120
  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 * 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"]}
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 * How long (ms) the card must be out of view before the same card can be\n * reported again. While a card stays in view it is reported only once.\n * @default 1500\n */\n repeatAfterMs?: number;\n /**\n * The card name is looked for only in the first N lines of text on the\n * card, top to bottom.\n * @default 3\n */\n nameLines?: number;\n /**\n * Words/phrases that are never the card name (card types, subtypes...).\n * A line made only of these words is skipped, so \"Trainer\" or \"Stage 1\" are\n * ignored but \"Energy Retrieval\" is still a name. Replaces the defaults.\n * @default ['Basic', 'Stage 1', 'Stage 2', 'Trainer', 'Item', 'Supporter', 'Stadium', 'Energy', 'Special Energy', 'Basic Energy', 'Pokémon', 'Pokémon Tool']\n */\n reservedWords?: string[];\n /**\n * Lines starting with any of these are never the card name.\n * Replaces the defaults.\n * @default ['Evolves from', 'HP']\n */\n reservedPrefixes?: string[];\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.3.1",
3
+ "version": "0.3.3",
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",