capacitor-card-scanner 0.3.1 → 0.3.2

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
@@ -340,6 +340,7 @@ requestPermissions() => Promise<PermissionStatus>
340
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
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
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> |
343
344
 
344
345
 
345
346
  #### PluginListenerHandle
@@ -64,6 +64,8 @@ public class CardDetector implements ImageAnalysis.Analyzer {
64
64
  public boolean debug = false;
65
65
  /** Outline (in grey) the objects that were not accepted as a card. */
66
66
  public boolean showRejected = true;
67
+ /** How long the card must be out of view before the same card can be reported again. */
68
+ public long repeatAfterMs = 1500;
67
69
  }
68
70
 
69
71
  /** An object found by ML Kit, with the values the card filters were checked against. */
@@ -108,8 +110,8 @@ public class CardDetector implements ImageAnalysis.Analyzer {
108
110
  );
109
111
  private static final Pattern HP_SUFFIX = Pattern.compile("\\s*HP\\s*\\d+\\s*$", Pattern.CASE_INSENSITIVE);
110
112
  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;
113
+ /** Names at least this similar are treated as the same card. */
114
+ private static final float SAME_NAME_SIMILARITY = 0.8f;
113
115
  /** Extra margin around the detected card before running OCR. */
114
116
  private static final float CROP_MARGIN = 0.04f;
115
117
 
@@ -126,7 +128,10 @@ public class CardDetector implements ImageAnalysis.Analyzer {
126
128
  private long lastSeenAt;
127
129
  private String candidateKey;
128
130
  private int candidateCount;
129
- private String lastEmittedKey;
131
+ // Last reported card. A reading is the same card if either its name or its
132
+ // number matches, because OCR does not read the number on every frame.
133
+ private String lastEmittedName;
134
+ private String lastEmittedNumber;
130
135
 
131
136
  public CardDetector(Options options, Listener listener) {
132
137
  this.options = options;
@@ -169,6 +174,9 @@ public class CardDetector implements ImageAnalysis.Analyzer {
169
174
  List<Candidate> candidates = new ArrayList<>();
170
175
  Rect card = pickCard(objects, frameW, frameH, candidates);
171
176
  RectF cardBox = card == null ? null : normalize(card, frameW, frameH);
177
+ // The card counts as "in view" while its outline is visible, even if
178
+ // its text is not readable on this frame.
179
+ if (card != null) lastSeenAt = now;
172
180
  BoxListener bl = boxListener;
173
181
  if (bl != null) bl.onDetections(cardBox, candidates, frameW, frameH);
174
182
 
@@ -287,8 +295,9 @@ public class CardDetector implements ImageAnalysis.Analyzer {
287
295
  candidateKey = key;
288
296
  candidateCount = 1;
289
297
  }
290
- if (candidateCount < options.stableFrames || key.equals(lastEmittedKey)) return;
291
- lastEmittedKey = key;
298
+ if (candidateCount < options.stableFrames || isLastEmitted(name, number)) return;
299
+ lastEmittedName = name;
300
+ lastEmittedNumber = number;
292
301
 
293
302
  JSObject result = new JSObject();
294
303
  result.put("name", name);
@@ -309,7 +318,33 @@ public class CardDetector implements ImageAnalysis.Analyzer {
309
318
  private void noCardRead(long now) {
310
319
  candidateKey = null;
311
320
  candidateCount = 0;
312
- if (now - lastSeenAt > RESET_AFTER_MS) lastEmittedKey = null;
321
+ if (now - lastSeenAt > options.repeatAfterMs) {
322
+ lastEmittedName = null;
323
+ lastEmittedNumber = null;
324
+ }
325
+ }
326
+
327
+ private boolean isLastEmitted(String name, String number) {
328
+ if (number != null && number.equals(lastEmittedNumber)) return true;
329
+ return name != null && lastEmittedName != null && similarity(name, lastEmittedName) >= SAME_NAME_SIMILARITY;
330
+ }
331
+
332
+ /** 0-1 similarity (LCS based), so an OCR slip like "Evolutlon" still matches "Evolution". */
333
+ private static float similarity(String a, String b) {
334
+ a = a.toLowerCase(Locale.ROOT);
335
+ b = b.toLowerCase(Locale.ROOT);
336
+ if (a.equals(b)) return 1f;
337
+ int[] prev = new int[b.length() + 1];
338
+ int[] cur = new int[b.length() + 1];
339
+ for (int i = 1; i <= a.length(); i++) {
340
+ for (int j = 1; j <= b.length(); j++) {
341
+ cur[j] = a.charAt(i - 1) == b.charAt(j - 1) ? prev[j - 1] + 1 : Math.max(prev[j], cur[j - 1]);
342
+ }
343
+ int[] tmp = prev;
344
+ prev = cur;
345
+ cur = tmp;
346
+ }
347
+ return 2f * prev[b.length()] / (a.length() + b.length());
313
348
  }
314
349
 
315
350
  private static JSObject box(float x, float y, float width, float height) {
@@ -102,6 +102,7 @@ public class CardScannerPlugin extends Plugin {
102
102
  options.requireCard = call.getBoolean("requireCard", options.requireCard);
103
103
  options.debug = call.getBoolean("debug", options.debug);
104
104
  options.showRejected = call.getBoolean("showRejected", options.showRejected);
105
+ options.repeatAfterMs = call.getInt("repeatAfterMs", (int) options.repeatAfterMs);
105
106
 
106
107
  CardDetector detector = new CardDetector(options, result -> notifyListeners("cardDetected", result));
107
108
  getActivity().runOnUiThread(() -> {
package/dist/docs.json CHANGED
@@ -420,6 +420,18 @@
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"
423
435
  }
424
436
  ]
425
437
  },
@@ -90,6 +90,12 @@ 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;
93
99
  }
94
100
  /** Normalized rectangle (0-1). */
95
101
  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\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.2",
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",