@parselo/capacitor-mrz 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'ParseloCapacitorMrz'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = package['license']
10
+ s.homepage = 'https://github.com/your-org/idscan-canada'
11
+ s.author = 'IDScan Canada'
12
+ s.source = { :git => 'https://github.com/your-org/idscan-canada.git', :tag => s.version.to_s }
13
+ s.source_files = 'ios/**/*.{swift,h,m}'
14
+ s.ios.deployment_target = '13.0'
15
+ s.dependency 'Capacitor'
16
+ s.swift_version = '5.1'
17
+ end
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # @parselo/capacitor-mrz
2
+
3
+ Capacitor plugin for on-device MRZ (Machine Readable Zone) text recognition.
4
+ Wraps Apple Vision (`VNRecognizeTextRequest`) on iOS and ML Kit Text Recognition
5
+ on Android. Returns the recognised text lines; pair with `@parselo/scanner-core`
6
+ to locate, parse, and validate the ICAO 9303 MRZ.
7
+
8
+ Supports all ICAO TD3 travel document types: standard passports (`P<`),
9
+ emergency travel documents (`PU`), permanent resident travel documents (`PR`),
10
+ and foreign passports.
11
+
12
+ ## Platform support
13
+
14
+ | Platform | Backend | Min version |
15
+ |---|---|---|
16
+ | iOS | Vision — `VNRecognizeTextRequest`, `.accurate` level | iOS 13 |
17
+ | Android | ML Kit Text Recognition v2, Latin script | API 21 |
18
+
19
+ Language correction is disabled on both platforms — it would corrupt MRZ
20
+ tokens and check digits.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install @parselo/capacitor-mrz @parselo/scanner-core
26
+ npx cap sync
27
+ ```
28
+
29
+ ### iOS — camera permission
30
+
31
+ Add to `ios/App/App/Info.plist` (if not already present):
32
+
33
+ ```xml
34
+ <key>NSCameraUsageDescription</key>
35
+ <string>Used to scan the MRZ on your travel document.</string>
36
+ ```
37
+
38
+ CocoaPods installs the `MrzPlugin.m` ObjC bridge automatically. If the plugin
39
+ throws "not implemented" at runtime, verify that `pod install` ran after
40
+ `cap sync` and that `MrzPlugin.m` is listed in the Xcode target's Compile
41
+ Sources.
42
+
43
+ ### Android
44
+
45
+ No extra configuration needed — the ML Kit dependency is declared in the
46
+ plugin's `build.gradle`.
47
+
48
+ ## API
49
+
50
+ ```ts
51
+ import { Mrz } from "@parselo/capacitor-mrz";
52
+
53
+ const { lines, raw } = await Mrz.recognizeText({ image });
54
+ // lines: string[] — one element per Vision/ML Kit text observation
55
+ // raw: string — lines joined with "\n"
56
+ ```
57
+
58
+ `image` accepts a `data:` URI, `file://` URI, bare absolute path, or raw base64 string.
59
+
60
+ Pass the full camera frame — the plugin sends the entire image to the OCR
61
+ engine. `scanner-core` locates the two 44-char MRZ lines anywhere in the
62
+ returned array and handles OCR noise in the surrounding biographical text.
63
+
64
+ ## Integration with scanner-core
65
+
66
+ ```ts
67
+ import { Scanner } from "@parselo/scanner-core";
68
+ import { Mrz } from "@parselo/capacitor-mrz";
69
+
70
+ const scanner = new Scanner({
71
+ license: {
72
+ token: "…",
73
+ bundleId: "com.example.app",
74
+ publicKeys: { "<kid>": "<base64-spki>" },
75
+ },
76
+ analytics: {
77
+ ingestUrl: "…",
78
+ store: { get, set },
79
+ },
80
+ native: { captureAndDecodePdf417: … }, // still needed for barcode scanning
81
+ mrzNative: {
82
+ captureAndRecognizeMrz: async () => {
83
+ const { lines } = await Mrz.recognizeText({ image: dataUrl });
84
+ return lines;
85
+ },
86
+ },
87
+ });
88
+
89
+ await scanner.init();
90
+ const result = await scanner.scanPassport();
91
+ // result.document.documentType === "passport"
92
+ // result.document.fields.firstName.value, .dateOfBirth.value, .country.value, …
93
+ ```
94
+
95
+ ### How name extraction works
96
+
97
+ OCR-B MRZ uses `<` as a word separator, which Vision and ML Kit frequently
98
+ misread as `C`, `S`, `K`, `@`, etc. `scanner-core` automatically cross-references
99
+ Vision's biographical-zone lines (the human-readable area above the MRZ, read
100
+ in a standard font) to recover correct first and last names even when the MRZ
101
+ separator characters are corrupted.
102
+
103
+ See the [full integration guide](https://github.com/parselo-io/parselo-sdk/blob/main/integration-guide.md) for a complete setup walkthrough including the camera UI component.
104
+
105
+ ## License
106
+
107
+ MIT
@@ -0,0 +1,49 @@
1
+ // Android build for @parselo/capacitor-mrz.
2
+ // SDK/AGP/Kotlin versions fall back to the host app's rootProject.ext when synced.
3
+
4
+ ext {
5
+ mrzMlkitVersion = project.hasProperty('mlkitTextVersion') ? rootProject.ext.mlkitTextVersion : '16.0.1'
6
+ }
7
+
8
+ buildscript {
9
+ ext.kotlin_version = project.hasProperty('kotlinVersion') ? rootProject.ext.kotlinVersion : '1.9.25'
10
+ repositories {
11
+ google()
12
+ mavenCentral()
13
+ }
14
+ dependencies {
15
+ classpath 'com.android.tools.build:gradle:8.2.1'
16
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
17
+ }
18
+ }
19
+
20
+ apply plugin: 'com.android.library'
21
+ apply plugin: 'kotlin-android'
22
+
23
+ android {
24
+ namespace "com.parselo.mrz"
25
+ compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
26
+ defaultConfig {
27
+ minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
28
+ targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34
29
+ }
30
+ compileOptions {
31
+ sourceCompatibility JavaVersion.VERSION_17
32
+ targetCompatibility JavaVersion.VERSION_17
33
+ }
34
+ kotlinOptions {
35
+ jvmTarget = '17'
36
+ }
37
+ }
38
+
39
+ repositories {
40
+ google()
41
+ mavenCentral()
42
+ }
43
+
44
+ dependencies {
45
+ implementation project(':capacitor-android')
46
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
47
+ // ML Kit Text Recognition v2 — Latin script; no network call, fully on-device.
48
+ implementation "com.google.mlkit:text-recognition:$mrzMlkitVersion"
49
+ }
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <!-- No permissions declared here: the plugin receives an already-captured
4
+ image and runs text recognition on it. Camera permission is the app's
5
+ responsibility, not the plugin's. -->
6
+ </manifest>
@@ -0,0 +1,72 @@
1
+ package com.parselo.mrz
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.BitmapFactory
5
+ import android.net.Uri
6
+ import android.util.Base64
7
+ import com.getcapacitor.JSArray
8
+ import com.getcapacitor.JSObject
9
+ import com.getcapacitor.Plugin
10
+ import com.getcapacitor.PluginCall
11
+ import com.getcapacitor.PluginMethod
12
+ import com.getcapacitor.annotation.CapacitorPlugin
13
+ import com.google.mlkit.vision.common.InputImage
14
+ import com.google.mlkit.vision.text.TextRecognition
15
+ import com.google.mlkit.vision.text.latin.TextRecognizerOptions
16
+
17
+ @CapacitorPlugin(name = "Mrz")
18
+ class MrzPlugin : Plugin() {
19
+
20
+ @PluginMethod
21
+ fun recognizeText(call: PluginCall) {
22
+ val imageStr = call.getString("image") ?: run {
23
+ call.reject("Missing 'image'"); return
24
+ }
25
+ val bitmap = loadBitmap(imageStr) ?: run {
26
+ call.reject("Could not load image from the provided string"); return
27
+ }
28
+ recognize(bitmap) { lines ->
29
+ val arr = JSArray()
30
+ lines.forEach { arr.put(it) }
31
+ val ret = JSObject()
32
+ ret.put("lines", arr)
33
+ ret.put("raw", lines.joinToString("\n"))
34
+ call.resolve(ret)
35
+ }
36
+ }
37
+
38
+ // ML Kit Text Recognition v2, Latin script, fully on-device.
39
+ private fun recognize(bitmap: Bitmap, callback: (List<String>) -> Unit) {
40
+ val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
41
+ val image = InputImage.fromBitmap(bitmap, 0)
42
+ recognizer.process(image)
43
+ .addOnSuccessListener { result ->
44
+ // Flatten to one entry per line so scanner-core can scan
45
+ // the array for consecutive 44-char TD3 lines.
46
+ val lines = result.textBlocks.flatMap { block ->
47
+ block.lines.map { line -> line.text }
48
+ }
49
+ callback(lines)
50
+ }
51
+ .addOnFailureListener { callback(emptyList()) }
52
+ }
53
+
54
+ // Image loading: file path, file:// URI, data: URI, or bare base64.
55
+ private fun loadBitmap(s: String): Bitmap? {
56
+ return try {
57
+ when {
58
+ s.startsWith("data:") -> {
59
+ val b64 = s.substringAfter(",", "")
60
+ val bytes = Base64.decode(b64, Base64.DEFAULT)
61
+ BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
62
+ }
63
+ s.startsWith("file://") -> BitmapFactory.decodeFile(Uri.parse(s).path)
64
+ s.startsWith("/") -> BitmapFactory.decodeFile(s)
65
+ else -> {
66
+ val bytes = Base64.decode(s, Base64.DEFAULT)
67
+ BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
68
+ }
69
+ }
70
+ } catch (e: Exception) { null }
71
+ }
72
+ }
@@ -0,0 +1,16 @@
1
+ export interface MrzPlugin {
2
+ /**
3
+ * Run on-device text recognition on an image and return all detected lines.
4
+ * Pass the result to scanner-core's parseMrz() to locate and parse the MRZ.
5
+ *
6
+ * @param options.image file path, file:// URI, data: URI, or bare base64 string
7
+ * @returns lines — every text line detected by Vision / ML Kit
8
+ * @returns raw — lines joined by "\n" (convenience for logging/debugging)
9
+ */
10
+ recognizeText(options: {
11
+ image: string;
12
+ }): Promise<{
13
+ lines: string[];
14
+ raw: string;
15
+ }>;
16
+ }
@@ -0,0 +1,6 @@
1
+ // definitions.ts
2
+ // Capacitor plugin interface for native MRZ text recognition.
3
+ // The plugin recognises text in an image and returns all detected lines.
4
+ // scanner-core is responsible for locating the MRZ within those lines and
5
+ // parsing it — keeping all parsing logic cross-platform in the TS core.
6
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { MrzPlugin } from "./definitions";
2
+ declare const Mrz: MrzPlugin;
3
+ export * from "./definitions";
4
+ export { Mrz };
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // index.ts
2
+ import { registerPlugin } from "@capacitor/core";
3
+ const Mrz = registerPlugin("Mrz", {
4
+ web: () => import("./web").then((m) => new m.MrzWeb()),
5
+ });
6
+ export * from "./definitions";
7
+ export { Mrz };
package/dist/web.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { WebPlugin } from "@capacitor/core";
2
+ import type { MrzPlugin } from "./definitions";
3
+ export declare class MrzWeb extends WebPlugin implements MrzPlugin {
4
+ recognizeText(): Promise<{
5
+ lines: string[];
6
+ raw: string;
7
+ }>;
8
+ }
package/dist/web.js ADDED
@@ -0,0 +1,7 @@
1
+ // web.ts
2
+ import { WebPlugin } from "@capacitor/core";
3
+ export class MrzWeb extends WebPlugin {
4
+ async recognizeText() {
5
+ throw this.unimplemented("MRZ text recognition is native-only — run on a device.");
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ #import <Foundation/Foundation.h>
2
+ #import <Capacitor/Capacitor.h>
3
+
4
+ // Registers the Swift plugin with Capacitor and exposes its methods to JS.
5
+ CAP_PLUGIN(MrzPlugin, "Mrz",
6
+ CAP_PLUGIN_METHOD(recognizeText, CAPPluginReturnPromise);
7
+ )
@@ -0,0 +1,100 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import Vision
4
+ import UIKit
5
+
6
+ @objc(MrzPlugin)
7
+ public class MrzPlugin: CAPPlugin {
8
+
9
+ @objc func recognizeText(_ call: CAPPluginCall) {
10
+ guard let imageStr = call.getString("image") else {
11
+ print("[MrzPlugin] ERROR: Missing 'image' parameter")
12
+ call.reject("Missing 'image'")
13
+ return
14
+ }
15
+ print("[MrzPlugin] recognizeText called, image string length: \(imageStr.count)")
16
+ guard let image = Self.loadImage(imageStr) else {
17
+ print("[MrzPlugin] ERROR: Could not load image from the provided string")
18
+ call.reject("Could not load image from the provided string")
19
+ return
20
+ }
21
+ print("[MrzPlugin] Image loaded: \(Int(image.size.width))×\(Int(image.size.height)), orientation: \(image.imageOrientation.rawValue)")
22
+ Self.recognize(in: image) { lines in
23
+ print("[MrzPlugin] Vision returned \(lines.count) line(s):")
24
+ lines.enumerated().forEach { i, l in print("[MrzPlugin] [\(i)] (\(l.count) chars): \(l)") }
25
+ call.resolve([
26
+ "lines": lines,
27
+ "raw": lines.joined(separator: "\n"),
28
+ ])
29
+ }
30
+ }
31
+
32
+ // MARK: - Vision text recognition
33
+
34
+ private static func recognize(in image: UIImage, completion: @escaping ([String]) -> Void) {
35
+ guard let cgImage = image.cgImage else { completion([]); return }
36
+
37
+ let request = VNRecognizeTextRequest { req, err in
38
+ if let err = err {
39
+ print("[MrzPlugin] Vision error: \(err.localizedDescription)")
40
+ completion([]); return
41
+ }
42
+ guard let results = req.results as? [VNRecognizedTextObservation] else {
43
+ print("[MrzPlugin] Vision: no results")
44
+ completion([]); return
45
+ }
46
+ print("[MrzPlugin] Vision: \(results.count) text observation(s)")
47
+ let lines = results.compactMap { $0.topCandidates(1).first?.string }
48
+ completion(lines)
49
+ }
50
+ // Accurate mode is required for MRZ — fast mode misses characters.
51
+ request.recognitionLevel = .accurate
52
+ // Language correction must be off: it replaces valid MRZ tokens (e.g. "<<")
53
+ // with natural-language words and corrupts check digits.
54
+ request.usesLanguageCorrection = false
55
+ request.recognitionLanguages = ["en-US"]
56
+
57
+ let orientation = cgOrientation(from: image.imageOrientation)
58
+ let handler = VNImageRequestHandler(cgImage: cgImage, orientation: orientation, options: [:])
59
+ do {
60
+ try handler.perform([request])
61
+ } catch {
62
+ completion([])
63
+ }
64
+ }
65
+
66
+ // MARK: - Image loading (file path, file:// URI, data: URI, or bare base64)
67
+
68
+ private static func loadImage(_ s: String) -> UIImage? {
69
+ if s.hasPrefix("data:") {
70
+ guard let comma = s.firstIndex(of: ","),
71
+ let data = Data(base64Encoded: String(s[s.index(after: comma)...])) else { return nil }
72
+ return UIImage(data: data)
73
+ }
74
+ if s.hasPrefix("file://") {
75
+ guard let path = URL(string: s)?.path else { return nil }
76
+ return UIImage(contentsOfFile: path)
77
+ }
78
+ if s.hasPrefix("/") {
79
+ return UIImage(contentsOfFile: s)
80
+ }
81
+ if let data = Data(base64Encoded: s) {
82
+ return UIImage(data: data)
83
+ }
84
+ return nil
85
+ }
86
+
87
+ private static func cgOrientation(from o: UIImage.Orientation) -> CGImagePropertyOrientation {
88
+ switch o {
89
+ case .up: return .up
90
+ case .down: return .down
91
+ case .left: return .left
92
+ case .right: return .right
93
+ case .upMirrored: return .upMirrored
94
+ case .downMirrored: return .downMirrored
95
+ case .leftMirrored: return .leftMirrored
96
+ case .rightMirrored: return .rightMirrored
97
+ @unknown default: return .up
98
+ }
99
+ }
100
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@parselo/capacitor-mrz",
3
+ "version": "0.2.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/parselo-io/parselo-sdk.git",
7
+ "directory": "packages/capacitor-mrz"
8
+ },
9
+ "description": "Native MRZ text recognition (Apple Vision / Android ML Kit) for Capacitor. Returns recognised lines; scanner-core locates and parses the MRZ.",
10
+ "license": "MIT",
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "type": "module",
15
+ "main": "dist/index.js",
16
+ "module": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "dist/",
27
+ "ios/",
28
+ "android/",
29
+ "ParseloCapacitorMrz.podspec",
30
+ "README.md"
31
+ ],
32
+ "scripts": {
33
+ "build": "tsc -p tsconfig.json"
34
+ },
35
+ "capacitor": {
36
+ "ios": {
37
+ "src": "ios"
38
+ },
39
+ "android": {
40
+ "src": "android"
41
+ }
42
+ },
43
+ "peerDependencies": {
44
+ "@capacitor/core": "^6.0.0"
45
+ },
46
+ "devDependencies": {
47
+ "@capacitor/core": "^6.0.0",
48
+ "typescript": "^5.5.0"
49
+ }
50
+ }