capacitor-card-scanner 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 = 'CapacitorCardScanner'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = package['license']
10
+ s.homepage = package['repository']['url']
11
+ s.author = package['author']
12
+ s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
+ s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
14
+ s.ios.deployment_target = '15.0'
15
+ s.dependency 'Capacitor'
16
+ s.swift_version = '5.1'
17
+ end
package/Package.swift ADDED
@@ -0,0 +1,28 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "CapacitorCardScanner",
6
+ platforms: [.iOS(.v15)],
7
+ products: [
8
+ .library(
9
+ name: "CapacitorCardScanner",
10
+ targets: ["CardScannerPlugin"])
11
+ ],
12
+ dependencies: [
13
+ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
14
+ ],
15
+ targets: [
16
+ .target(
17
+ name: "CardScannerPlugin",
18
+ dependencies: [
19
+ .product(name: "Capacitor", package: "capacitor-swift-pm"),
20
+ .product(name: "Cordova", package: "capacitor-swift-pm")
21
+ ],
22
+ path: "ios/Sources/CardScannerPlugin"),
23
+ .testTarget(
24
+ name: "CardScannerPluginTests",
25
+ dependencies: ["CardScannerPlugin"],
26
+ path: "ios/Tests/CardScannerPluginTests")
27
+ ]
28
+ )
package/README.md ADDED
@@ -0,0 +1,268 @@
1
+ # capacitor-card-scanner
2
+
3
+ Recognize a pokemon card with camera open in background
4
+
5
+ ## Install
6
+
7
+ To use npm
8
+
9
+ ```bash
10
+ npm install capacitor-card-scanner
11
+ ````
12
+
13
+ To use yarn
14
+
15
+ ```bash
16
+ yarn add capacitor-card-scanner
17
+ ```
18
+
19
+ Sync native files
20
+
21
+ ```bash
22
+ npx cap sync
23
+ ```
24
+
25
+ ## How it works
26
+
27
+ The native camera preview is placed **behind** the WebView and the WebView is
28
+ made transparent. Your HTML UI (buttons, overlays, the card frame) is drawn on
29
+ top of the live camera feed.
30
+
31
+ For the camera to be visible, **every element between the page and the camera
32
+ must have a transparent background** (`html`, `body` and the layout containers).
33
+
34
+ ### iOS
35
+
36
+ Add to the app's `Info.plist`:
37
+
38
+ ```xml
39
+ <key>NSCameraUsageDescription</key>
40
+ <string>The camera is used to scan cards</string>
41
+ ```
42
+
43
+ ### OutSystems
44
+
45
+ 1. Add a CSS class to the Theme / screen:
46
+
47
+ ```css
48
+ body.camera-active,
49
+ body.camera-active .layout,
50
+ body.camera-active .main,
51
+ body.camera-active .main-content,
52
+ body.camera-active .content,
53
+ body.camera-active .screen,
54
+ body.camera-active .active-screen {
55
+ background: transparent !important;
56
+ }
57
+ ```
58
+
59
+ 2. On the screen `OnReady` (JavaScript node):
60
+
61
+ ```js
62
+ const { CardScanner } = Capacitor.Plugins;
63
+ document.body.classList.add('camera-active');
64
+ await CardScanner.start({ position: 'rear' });
65
+ ```
66
+
67
+ 3. On `OnDestroy`:
68
+
69
+ ```js
70
+ document.body.classList.remove('camera-active');
71
+ await Capacitor.Plugins.CardScanner.stop();
72
+ ```
73
+
74
+ 4. Button to scan:
75
+
76
+ ```js
77
+ const photo = await Capacitor.Plugins.CardScanner.capture({ quality: 80 });
78
+ $parameters.ImageBase64 = photo.base64;
79
+ ```
80
+
81
+ ## API
82
+
83
+ <docgen-index>
84
+
85
+ * [`start(...)`](#start)
86
+ * [`stop()`](#stop)
87
+ * [`capture(...)`](#capture)
88
+ * [`setTorch(...)`](#settorch)
89
+ * [`setZoom(...)`](#setzoom)
90
+ * [`flip()`](#flip)
91
+ * [`checkPermissions()`](#checkpermissions)
92
+ * [`requestPermissions()`](#requestpermissions)
93
+ * [Interfaces](#interfaces)
94
+ * [Type Aliases](#type-aliases)
95
+
96
+ </docgen-index>
97
+
98
+ <docgen-api>
99
+ <!--Update the source file JSDoc comments and rerun docgen to update the docs below-->
100
+
101
+ ### start(...)
102
+
103
+ ```typescript
104
+ start(options?: StartOptions | undefined) => Promise<void>
105
+ ```
106
+
107
+ Opens the camera **behind** the WebView. The WebView background is made
108
+ transparent, so the HTML page must also use transparent backgrounds
109
+ where the camera should be visible.
110
+
111
+ | Param | Type |
112
+ | ------------- | ----------------------------------------------------- |
113
+ | **`options`** | <code><a href="#startoptions">StartOptions</a></code> |
114
+
115
+ --------------------
116
+
117
+
118
+ ### stop()
119
+
120
+ ```typescript
121
+ stop() => Promise<void>
122
+ ```
123
+
124
+ Stops the camera and removes the preview.
125
+
126
+ --------------------
127
+
128
+
129
+ ### capture(...)
130
+
131
+ ```typescript
132
+ capture(options?: CaptureOptions | undefined) => Promise<CaptureResult>
133
+ ```
134
+
135
+ Takes a photo from the running camera.
136
+
137
+ | Param | Type |
138
+ | ------------- | --------------------------------------------------------- |
139
+ | **`options`** | <code><a href="#captureoptions">CaptureOptions</a></code> |
140
+
141
+ **Returns:** <code>Promise&lt;<a href="#captureresult">CaptureResult</a>&gt;</code>
142
+
143
+ --------------------
144
+
145
+
146
+ ### setTorch(...)
147
+
148
+ ```typescript
149
+ setTorch(options: TorchOptions) => Promise<void>
150
+ ```
151
+
152
+ Turns the flashlight on/off.
153
+
154
+ | Param | Type |
155
+ | ------------- | ----------------------------------------------------- |
156
+ | **`options`** | <code><a href="#torchoptions">TorchOptions</a></code> |
157
+
158
+ --------------------
159
+
160
+
161
+ ### setZoom(...)
162
+
163
+ ```typescript
164
+ setZoom(options: ZoomOptions) => Promise<void>
165
+ ```
166
+
167
+ Sets the zoom ratio.
168
+
169
+ | Param | Type |
170
+ | ------------- | --------------------------------------------------- |
171
+ | **`options`** | <code><a href="#zoomoptions">ZoomOptions</a></code> |
172
+
173
+ --------------------
174
+
175
+
176
+ ### flip()
177
+
178
+ ```typescript
179
+ flip() => Promise<void>
180
+ ```
181
+
182
+ Switches between the rear and front camera.
183
+
184
+ --------------------
185
+
186
+
187
+ ### checkPermissions()
188
+
189
+ ```typescript
190
+ checkPermissions() => Promise<PermissionStatus>
191
+ ```
192
+
193
+ **Returns:** <code>Promise&lt;<a href="#permissionstatus">PermissionStatus</a>&gt;</code>
194
+
195
+ --------------------
196
+
197
+
198
+ ### requestPermissions()
199
+
200
+ ```typescript
201
+ requestPermissions() => Promise<PermissionStatus>
202
+ ```
203
+
204
+ **Returns:** <code>Promise&lt;<a href="#permissionstatus">PermissionStatus</a>&gt;</code>
205
+
206
+ --------------------
207
+
208
+
209
+ ### Interfaces
210
+
211
+
212
+ #### StartOptions
213
+
214
+ | Prop | Type | Description | Default |
215
+ | -------------- | --------------------------------------------------------- | -------------------- | ------------------- |
216
+ | **`position`** | <code><a href="#cameraposition">CameraPosition</a></code> | Which camera to use. | <code>'rear'</code> |
217
+
218
+
219
+ #### CaptureResult
220
+
221
+ | Prop | Type | Description |
222
+ | ------------ | ------------------- | ------------------------------------------------- |
223
+ | **`base64`** | <code>string</code> | Base64 encoded JPEG (without the `data:` prefix). |
224
+ | **`width`** | <code>number</code> | |
225
+ | **`height`** | <code>number</code> | |
226
+
227
+
228
+ #### CaptureOptions
229
+
230
+ | Prop | Type | Description | Default |
231
+ | ------------- | ------------------- | --------------------- | --------------- |
232
+ | **`quality`** | <code>number</code> | JPEG quality (0-100). | <code>85</code> |
233
+
234
+
235
+ #### TorchOptions
236
+
237
+ | Prop | Type |
238
+ | ------------- | -------------------- |
239
+ | **`enabled`** | <code>boolean</code> |
240
+
241
+
242
+ #### ZoomOptions
243
+
244
+ | Prop | Type | Description |
245
+ | ----------- | ------------------- | ----------------------------------------------------- |
246
+ | **`ratio`** | <code>number</code> | Zoom ratio, clamped between the camera's min and max. |
247
+
248
+
249
+ #### PermissionStatus
250
+
251
+ | Prop | Type |
252
+ | ------------ | ----------------------------------------------------------- |
253
+ | **`camera`** | <code><a href="#permissionstate">PermissionState</a></code> |
254
+
255
+
256
+ ### Type Aliases
257
+
258
+
259
+ #### CameraPosition
260
+
261
+ <code>'rear' | 'front'</code>
262
+
263
+
264
+ #### PermissionState
265
+
266
+ <code>'prompt' | 'prompt-with-rationale' | 'granted' | 'denied'</code>
267
+
268
+ </docgen-api>
@@ -0,0 +1,64 @@
1
+ ext {
2
+ junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
+ androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
4
+ androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
5
+ androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
6
+ }
7
+
8
+ buildscript {
9
+ repositories {
10
+ google()
11
+ mavenCentral()
12
+ }
13
+ dependencies {
14
+ classpath 'com.android.tools.build:gradle:8.13.0'
15
+ }
16
+ }
17
+
18
+ apply plugin: 'com.android.library'
19
+
20
+ android {
21
+ namespace = "pt.tcgclassifier.cardscanner"
22
+ compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
23
+ defaultConfig {
24
+ minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
25
+ targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
26
+ versionCode 1
27
+ versionName "1.0"
28
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
29
+ }
30
+ buildTypes {
31
+ release {
32
+ minifyEnabled false
33
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
34
+ }
35
+ }
36
+ lintOptions {
37
+ abortOnError = false
38
+ }
39
+ compileOptions {
40
+ sourceCompatibility JavaVersion.VERSION_21
41
+ targetCompatibility JavaVersion.VERSION_21
42
+ }
43
+ }
44
+
45
+
46
+ repositories {
47
+ google()
48
+ mavenCentral()
49
+ }
50
+
51
+
52
+ dependencies {
53
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
54
+ implementation project(':capacitor-android')
55
+ implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
56
+ testImplementation "junit:junit:$junitVersion"
57
+ androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
58
+ androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
59
+ def camerax_version = "1.4.2"
60
+ implementation "androidx.camera:camera-core:$camerax_version"
61
+ implementation "androidx.camera:camera-camera2:$camerax_version"
62
+ implementation "androidx.camera:camera-lifecycle:$camerax_version"
63
+ implementation "androidx.camera:camera-view:$camerax_version"
64
+ }
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.CAMERA" />
3
+ </manifest>
@@ -0,0 +1,195 @@
1
+ package pt.tcgclassifier.cardscanner;
2
+
3
+ import android.graphics.Bitmap;
4
+ import android.graphics.BitmapFactory;
5
+ import android.graphics.Color;
6
+ import android.graphics.Matrix;
7
+ import android.util.Base64;
8
+ import android.view.ViewGroup;
9
+ import android.webkit.WebView;
10
+ import androidx.annotation.NonNull;
11
+ import androidx.appcompat.app.AppCompatActivity;
12
+ import androidx.camera.core.Camera;
13
+ import androidx.camera.core.CameraSelector;
14
+ import androidx.camera.core.ImageCapture;
15
+ import androidx.camera.core.ImageCaptureException;
16
+ import androidx.camera.core.ImageProxy;
17
+ import androidx.camera.core.Preview;
18
+ import androidx.camera.core.ZoomState;
19
+ import androidx.camera.lifecycle.ProcessCameraProvider;
20
+ import androidx.camera.view.PreviewView;
21
+ import androidx.core.content.ContextCompat;
22
+ import com.google.common.util.concurrent.ListenableFuture;
23
+ import java.io.ByteArrayOutputStream;
24
+ import java.nio.ByteBuffer;
25
+
26
+ /**
27
+ * Places a CameraX preview behind the Capacitor WebView and makes the WebView
28
+ * transparent, so the HTML UI is drawn on top of the camera feed.
29
+ */
30
+ public class CardScanner {
31
+
32
+ public interface Callback<T> {
33
+ void onSuccess(T result);
34
+ void onError(String message, Exception e);
35
+ }
36
+
37
+ public static class Photo {
38
+
39
+ public final String base64;
40
+ public final int width;
41
+ public final int height;
42
+
43
+ Photo(String base64, int width, int height) {
44
+ this.base64 = base64;
45
+ this.width = width;
46
+ this.height = height;
47
+ }
48
+ }
49
+
50
+ private final AppCompatActivity activity;
51
+ private final WebView webView;
52
+
53
+ private PreviewView previewView;
54
+ private ProcessCameraProvider cameraProvider;
55
+ private ImageCapture imageCapture;
56
+ private Camera camera;
57
+ private boolean useFront = false;
58
+
59
+ public CardScanner(AppCompatActivity activity, WebView webView) {
60
+ this.activity = activity;
61
+ this.webView = webView;
62
+ }
63
+
64
+ public boolean isRunning() {
65
+ return camera != null;
66
+ }
67
+
68
+ public boolean isFront() {
69
+ return useFront;
70
+ }
71
+
72
+ /** Must be called on the UI thread. */
73
+ public void start(boolean front, Callback<Void> callback) {
74
+ useFront = front;
75
+
76
+ if (previewView == null) {
77
+ previewView = new PreviewView(activity);
78
+ // TextureView-based mode composes correctly underneath a transparent WebView.
79
+ previewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
80
+ previewView.setScaleType(PreviewView.ScaleType.FILL_CENTER);
81
+ ViewGroup parent = (ViewGroup) webView.getParent();
82
+ parent.addView(previewView, 0, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
83
+ }
84
+
85
+ webView.setBackgroundColor(Color.TRANSPARENT);
86
+
87
+ ListenableFuture<ProcessCameraProvider> future = ProcessCameraProvider.getInstance(activity);
88
+ future.addListener(
89
+ () -> {
90
+ try {
91
+ cameraProvider = future.get();
92
+ bind();
93
+ callback.onSuccess(null);
94
+ } catch (Exception e) {
95
+ callback.onError("Failed to start camera: " + e.getMessage(), e);
96
+ }
97
+ },
98
+ ContextCompat.getMainExecutor(activity)
99
+ );
100
+ }
101
+
102
+ private void bind() {
103
+ cameraProvider.unbindAll();
104
+
105
+ Preview preview = new Preview.Builder().build();
106
+ preview.setSurfaceProvider(previewView.getSurfaceProvider());
107
+
108
+ imageCapture = new ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY).build();
109
+
110
+ CameraSelector selector = useFront ? CameraSelector.DEFAULT_FRONT_CAMERA : CameraSelector.DEFAULT_BACK_CAMERA;
111
+ camera = cameraProvider.bindToLifecycle(activity, selector, preview, imageCapture);
112
+ }
113
+
114
+ /** Must be called on the UI thread. */
115
+ public void stop() {
116
+ if (cameraProvider != null) {
117
+ cameraProvider.unbindAll();
118
+ }
119
+ camera = null;
120
+ imageCapture = null;
121
+ if (previewView != null) {
122
+ ((ViewGroup) previewView.getParent()).removeView(previewView);
123
+ previewView = null;
124
+ }
125
+ webView.setBackgroundColor(Color.WHITE);
126
+ }
127
+
128
+ /** Must be called on the UI thread. */
129
+ public void flip(Callback<Void> callback) {
130
+ useFront = !useFront;
131
+ try {
132
+ bind();
133
+ callback.onSuccess(null);
134
+ } catch (Exception e) {
135
+ callback.onError("Failed to flip camera: " + e.getMessage(), e);
136
+ }
137
+ }
138
+
139
+ public void setTorch(boolean enabled) {
140
+ camera.getCameraControl().enableTorch(enabled);
141
+ }
142
+
143
+ public void setZoom(float ratio) {
144
+ ZoomState state = camera.getCameraInfo().getZoomState().getValue();
145
+ if (state != null) {
146
+ ratio = Math.max(state.getMinZoomRatio(), Math.min(state.getMaxZoomRatio(), ratio));
147
+ }
148
+ camera.getCameraControl().setZoomRatio(ratio);
149
+ }
150
+
151
+ public void capture(int quality, Callback<Photo> callback) {
152
+ imageCapture.takePicture(
153
+ ContextCompat.getMainExecutor(activity),
154
+ new ImageCapture.OnImageCapturedCallback() {
155
+ @Override
156
+ public void onCaptureSuccess(@NonNull ImageProxy image) {
157
+ try {
158
+ callback.onSuccess(toPhoto(image, quality));
159
+ } catch (Exception e) {
160
+ callback.onError("Failed to process image: " + e.getMessage(), e);
161
+ } finally {
162
+ image.close();
163
+ }
164
+ }
165
+
166
+ @Override
167
+ public void onError(@NonNull ImageCaptureException e) {
168
+ callback.onError("Capture failed: " + e.getMessage(), e);
169
+ }
170
+ }
171
+ );
172
+ }
173
+
174
+ private Photo toPhoto(ImageProxy image, int quality) {
175
+ ByteBuffer buffer = image.getPlanes()[0].getBuffer();
176
+ byte[] jpeg = new byte[buffer.remaining()];
177
+ buffer.get(jpeg);
178
+
179
+ Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length);
180
+ int rotation = image.getImageInfo().getRotationDegrees();
181
+ if (rotation != 0) {
182
+ Matrix matrix = new Matrix();
183
+ matrix.postRotate(rotation);
184
+ Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
185
+ bitmap.recycle();
186
+ bitmap = rotated;
187
+ }
188
+
189
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
190
+ bitmap.compress(Bitmap.CompressFormat.JPEG, quality, out);
191
+ Photo photo = new Photo(Base64.encodeToString(out.toByteArray(), Base64.NO_WRAP), bitmap.getWidth(), bitmap.getHeight());
192
+ bitmap.recycle();
193
+ return photo;
194
+ }
195
+ }