cordova-plugin-mlkit-suite 0.1.0 → 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.
- package/CHANGELOG.md +12 -0
- package/README.md +526 -640
- package/docs/screenshots/ios-text-recognition.jpg +0 -0
- package/package.json +7 -3
- package/plugin.xml +149 -134
- package/src/android/java/BarcodeFrameAnalyzer.java +115 -111
- package/src/android/java/BarcodeOverlayView.java +83 -25
- package/src/android/java/BarcodeScanActivity.java +100 -12
- package/src/android/java/BarcodeScanConfig.java +6 -1
- package/src/android/java/BarcodeScanRegion.java +26 -8
- package/src/android/java/MlKitModelsModuleInstaller.java +35 -16
- package/src/android/java/MlKitSuitePlugin.java +315 -33
- package/src/android/java/SelfieSegmentationProcessor.java +215 -0
- package/src/android/java/StaticImageProcessor.java +208 -151
- package/src/android/java/SubjectSegmentationModuleInstaller.java +154 -0
- package/src/android/java/SubjectSegmentationProcessor.java +189 -0
- package/src/android/mlkit-suite.gradle +30 -0
- package/src/android/res/values/mlkit_suite_strings.xml +8 -0
- package/src/android/res/values-es/mlkit_suite_strings.xml +8 -0
- package/src/ios/BarcodeOverlayView.swift +57 -6
- package/src/ios/BarcodeScanTypes.swift +3 -1
- package/src/ios/BarcodeScanViewController.swift +120 -5
- package/src/ios/MlKitSuitePlugin.swift +222 -13
- package/src/ios/MlKitSuiteResources.bundle/en.lproj/Localizable.strings +4 -0
- package/src/ios/MlKitSuiteResources.bundle/es.lproj/Localizable.strings +4 -0
- package/src/ios/SelfieSegmentationProcessor.swift +105 -0
- package/src/ios/TextRecognitionProcessor.swift +99 -0
- package/tests/ios-image-labeling.test.js +17 -0
- package/tests/ios-live-barcode.test.js +5 -0
- package/tests/ios-selfie-segmentation.test.js +24 -0
- package/tests/ios-static-text.test.js +21 -0
- package/tests/java/BarcodeScanRegionTest.java +12 -0
- package/tests/java-confirmation.test.js +41 -41
- package/tests/js-api.test.js +42 -2
- package/www/MlKit.js +111 -7
- package/docs/screenshots/capabilities.jpg +0 -0
- package/docs/screenshots/demo-tour.gif +0 -0
- package/docs/screenshots/text-recognition.jpg +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
package com.andreszs.plugins.mlkitsuite;
|
|
2
|
+
|
|
3
|
+
import android.app.Activity;
|
|
4
|
+
|
|
5
|
+
import com.google.android.gms.common.moduleinstall.InstallStatusListener;
|
|
6
|
+
import com.google.android.gms.common.moduleinstall.ModuleInstall;
|
|
7
|
+
import com.google.android.gms.common.moduleinstall.ModuleInstallClient;
|
|
8
|
+
import com.google.android.gms.common.moduleinstall.ModuleInstallRequest;
|
|
9
|
+
import com.google.android.gms.common.moduleinstall.ModuleInstallStatusCodes;
|
|
10
|
+
import com.google.android.gms.common.moduleinstall.ModuleInstallStatusUpdate;
|
|
11
|
+
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation;
|
|
12
|
+
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenter;
|
|
13
|
+
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions;
|
|
14
|
+
|
|
15
|
+
import org.json.JSONObject;
|
|
16
|
+
|
|
17
|
+
import java.util.concurrent.atomic.AtomicBoolean;
|
|
18
|
+
|
|
19
|
+
final class SubjectSegmentationModuleInstaller {
|
|
20
|
+
interface Callback {
|
|
21
|
+
void onReady(JSONObject result);
|
|
22
|
+
void onError(JSONObject error);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface AvailabilityCallback {
|
|
26
|
+
void onResult(Boolean ready);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private SubjectSegmentationModuleInstaller() { }
|
|
30
|
+
|
|
31
|
+
static void checkAvailability(Activity activity, AvailabilityCallback callback) {
|
|
32
|
+
SubjectSegmenter segmenter = segmenter();
|
|
33
|
+
ModuleInstall.getClient(activity).areModulesAvailable(segmenter)
|
|
34
|
+
.addOnSuccessListener(response -> {
|
|
35
|
+
segmenter.close();
|
|
36
|
+
callback.onResult(response.areModulesAvailable());
|
|
37
|
+
})
|
|
38
|
+
.addOnFailureListener(error -> {
|
|
39
|
+
segmenter.close();
|
|
40
|
+
callback.onResult(null);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static void prepare(Activity activity, String loadingMessage,
|
|
45
|
+
MlKitLocalization localization, Callback callback) {
|
|
46
|
+
SubjectSegmenter segmenter = segmenter();
|
|
47
|
+
ModuleInstallClient client = ModuleInstall.getClient(activity);
|
|
48
|
+
client.areModulesAvailable(segmenter)
|
|
49
|
+
.addOnSuccessListener(response -> {
|
|
50
|
+
if (response.areModulesAvailable()) {
|
|
51
|
+
segmenter.close();
|
|
52
|
+
callback.onReady(readyResult(true));
|
|
53
|
+
} else {
|
|
54
|
+
MlKitLoadingDialog loadingDialog = new MlKitLoadingDialog(
|
|
55
|
+
activity, loadingMessage
|
|
56
|
+
);
|
|
57
|
+
loadingDialog.show();
|
|
58
|
+
install(client, segmenter, loadingDialog, localization, callback);
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
.addOnFailureListener(error -> {
|
|
62
|
+
segmenter.close();
|
|
63
|
+
callback.onError(MlKitSuitePlugin.error(
|
|
64
|
+
"MODULE_UNAVAILABLE",
|
|
65
|
+
localization.text("subject_check_failed"),
|
|
66
|
+
true
|
|
67
|
+
));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private static SubjectSegmenter segmenter() {
|
|
72
|
+
return SubjectSegmentation.getClient(
|
|
73
|
+
new SubjectSegmenterOptions.Builder().enableForegroundBitmap().build()
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private static void install(ModuleInstallClient client, SubjectSegmenter segmenter,
|
|
78
|
+
MlKitLoadingDialog loadingDialog,
|
|
79
|
+
MlKitLocalization localization, Callback callback) {
|
|
80
|
+
AtomicBoolean finished = new AtomicBoolean(false);
|
|
81
|
+
InstallStatusListener[] listenerHolder = new InstallStatusListener[1];
|
|
82
|
+
listenerHolder[0] = update -> {
|
|
83
|
+
int state = update.getInstallState();
|
|
84
|
+
if (state == ModuleInstallStatusUpdate.InstallState.STATE_COMPLETED) {
|
|
85
|
+
finishSuccess(
|
|
86
|
+
client, listenerHolder[0], segmenter, loadingDialog, callback, finished, false
|
|
87
|
+
);
|
|
88
|
+
} else if (state == ModuleInstallStatusUpdate.InstallState.STATE_CANCELED) {
|
|
89
|
+
finishError(
|
|
90
|
+
client, listenerHolder[0], segmenter, loadingDialog, callback, finished,
|
|
91
|
+
"MODULE_DOWNLOAD_CANCELLED",
|
|
92
|
+
localization.text("subject_download_cancelled")
|
|
93
|
+
);
|
|
94
|
+
} else if (state == ModuleInstallStatusUpdate.InstallState.STATE_FAILED) {
|
|
95
|
+
String status = ModuleInstallStatusCodes.getStatusCodeString(update.getErrorCode());
|
|
96
|
+
finishError(
|
|
97
|
+
client, listenerHolder[0], segmenter, loadingDialog, callback, finished,
|
|
98
|
+
"MODULE_DOWNLOAD_FAILED",
|
|
99
|
+
localization.format("subject_download_failed", status)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
ModuleInstallRequest request = ModuleInstallRequest.newBuilder()
|
|
105
|
+
.addApi(segmenter)
|
|
106
|
+
.setListener(listenerHolder[0])
|
|
107
|
+
.build();
|
|
108
|
+
client.installModules(request)
|
|
109
|
+
.addOnSuccessListener(response -> {
|
|
110
|
+
if (response.areModulesAlreadyInstalled()) {
|
|
111
|
+
finishSuccess(
|
|
112
|
+
client, listenerHolder[0], segmenter, loadingDialog,
|
|
113
|
+
callback, finished, true
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
.addOnFailureListener(error -> finishError(
|
|
118
|
+
client, listenerHolder[0], segmenter, loadingDialog, callback, finished,
|
|
119
|
+
"MODULE_DOWNLOAD_FAILED",
|
|
120
|
+
localization.text("subject_download_start_failed")
|
|
121
|
+
));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private static void finishSuccess(ModuleInstallClient client, InstallStatusListener listener,
|
|
125
|
+
SubjectSegmenter segmenter,
|
|
126
|
+
MlKitLoadingDialog loadingDialog, Callback callback,
|
|
127
|
+
AtomicBoolean finished, boolean alreadyAvailable) {
|
|
128
|
+
if (!finished.compareAndSet(false, true)) return;
|
|
129
|
+
client.unregisterListener(listener);
|
|
130
|
+
loadingDialog.dismiss();
|
|
131
|
+
segmenter.close();
|
|
132
|
+
callback.onReady(readyResult(alreadyAvailable));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private static void finishError(ModuleInstallClient client, InstallStatusListener listener,
|
|
136
|
+
SubjectSegmenter segmenter,
|
|
137
|
+
MlKitLoadingDialog loadingDialog, Callback callback,
|
|
138
|
+
AtomicBoolean finished, String code, String message) {
|
|
139
|
+
if (!finished.compareAndSet(false, true)) return;
|
|
140
|
+
client.unregisterListener(listener);
|
|
141
|
+
loadingDialog.dismiss();
|
|
142
|
+
segmenter.close();
|
|
143
|
+
callback.onError(MlKitSuitePlugin.error(code, message, true));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private static JSONObject readyResult(boolean alreadyAvailable) {
|
|
147
|
+
JSONObject result = new JSONObject();
|
|
148
|
+
try {
|
|
149
|
+
result.put("status", "ready");
|
|
150
|
+
result.put("alreadyAvailable", alreadyAvailable);
|
|
151
|
+
} catch (Exception ignored) { }
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
package com.andreszs.plugins.mlkitsuite;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.graphics.Bitmap;
|
|
5
|
+
import android.graphics.BitmapFactory;
|
|
6
|
+
import android.graphics.Matrix;
|
|
7
|
+
import android.net.Uri;
|
|
8
|
+
|
|
9
|
+
import androidx.exifinterface.media.ExifInterface;
|
|
10
|
+
|
|
11
|
+
import com.google.mlkit.vision.common.InputImage;
|
|
12
|
+
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation;
|
|
13
|
+
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenter;
|
|
14
|
+
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions;
|
|
15
|
+
|
|
16
|
+
import java.io.File;
|
|
17
|
+
import java.io.FileOutputStream;
|
|
18
|
+
import java.io.InputStream;
|
|
19
|
+
import java.util.UUID;
|
|
20
|
+
import java.util.concurrent.Executor;
|
|
21
|
+
|
|
22
|
+
final class SubjectSegmentationProcessor {
|
|
23
|
+
interface Callback {
|
|
24
|
+
void onSuccess(Result result);
|
|
25
|
+
void onFailure(String code, String message, boolean recoverable);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static final class Result {
|
|
29
|
+
final File file;
|
|
30
|
+
final int width;
|
|
31
|
+
final int height;
|
|
32
|
+
|
|
33
|
+
Result(File file, int width, int height) {
|
|
34
|
+
this.file = file;
|
|
35
|
+
this.width = width;
|
|
36
|
+
this.height = height;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private SubjectSegmentationProcessor() { }
|
|
41
|
+
|
|
42
|
+
static void process(Context context, String rawPath, File cacheDirectory, int maxDimension,
|
|
43
|
+
Executor callbackExecutor, MlKitLocalization localization,
|
|
44
|
+
Callback callback) {
|
|
45
|
+
final Bitmap source;
|
|
46
|
+
try {
|
|
47
|
+
Uri uri = StaticImageProcessor.resolveUri(rawPath, cacheDirectory);
|
|
48
|
+
source = loadOrientedBitmap(context, uri, maxDimension);
|
|
49
|
+
} catch (Exception exception) {
|
|
50
|
+
callback.onFailure("INVALID_IMAGE", localization.text("image_open_failed"), false);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
SubjectSegmenterOptions options = new SubjectSegmenterOptions.Builder()
|
|
55
|
+
.enableForegroundBitmap()
|
|
56
|
+
.build();
|
|
57
|
+
SubjectSegmenter segmenter = SubjectSegmentation.getClient(options);
|
|
58
|
+
segmenter.process(InputImage.fromBitmap(source, 0))
|
|
59
|
+
.addOnSuccessListener(callbackExecutor, result -> {
|
|
60
|
+
Bitmap foreground = result.getForegroundBitmap();
|
|
61
|
+
try {
|
|
62
|
+
if (foreground == null) {
|
|
63
|
+
throw new IllegalStateException("ML Kit returned no foreground bitmap.");
|
|
64
|
+
}
|
|
65
|
+
callback.onSuccess(writePng(foreground, cacheDirectory));
|
|
66
|
+
} catch (Exception exception) {
|
|
67
|
+
callback.onFailure(
|
|
68
|
+
"PROCESSING_FAILED",
|
|
69
|
+
localization.text("subject_segmentation_failed"),
|
|
70
|
+
true
|
|
71
|
+
);
|
|
72
|
+
} finally {
|
|
73
|
+
if (foreground != null && foreground != source) foreground.recycle();
|
|
74
|
+
source.recycle();
|
|
75
|
+
segmenter.close();
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
.addOnFailureListener(callbackExecutor, error -> {
|
|
79
|
+
source.recycle();
|
|
80
|
+
segmenter.close();
|
|
81
|
+
callback.onFailure(
|
|
82
|
+
"PROCESSING_FAILED",
|
|
83
|
+
localization.text("subject_segmentation_failed"),
|
|
84
|
+
true
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private static Result writePng(Bitmap bitmap, File directory) throws Exception {
|
|
90
|
+
if (!directory.exists() && !directory.mkdirs()) {
|
|
91
|
+
throw new IllegalStateException("Unable to create the image cache directory.");
|
|
92
|
+
}
|
|
93
|
+
File destination = new File(
|
|
94
|
+
directory,
|
|
95
|
+
"subject-" + UUID.randomUUID().toString().toLowerCase() + ".png"
|
|
96
|
+
);
|
|
97
|
+
try (FileOutputStream stream = new FileOutputStream(destination)) {
|
|
98
|
+
if (!bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
|
|
99
|
+
throw new IllegalStateException("Unable to encode the segmented PNG.");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return new Result(destination, bitmap.getWidth(), bitmap.getHeight());
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private static Bitmap loadOrientedBitmap(Context context, Uri uri, int maxDimension)
|
|
106
|
+
throws Exception {
|
|
107
|
+
BitmapFactory.Options bounds = new BitmapFactory.Options();
|
|
108
|
+
bounds.inJustDecodeBounds = true;
|
|
109
|
+
try (InputStream stream = context.getContentResolver().openInputStream(uri)) {
|
|
110
|
+
if (stream == null) throw new IllegalStateException("Unable to open image.");
|
|
111
|
+
BitmapFactory.decodeStream(stream, null, bounds);
|
|
112
|
+
}
|
|
113
|
+
int sampleSize = 1;
|
|
114
|
+
int largestBound = Math.max(bounds.outWidth, bounds.outHeight);
|
|
115
|
+
while (largestBound > 0 && largestBound / sampleSize > maxDimension * 2) {
|
|
116
|
+
sampleSize *= 2;
|
|
117
|
+
}
|
|
118
|
+
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
|
|
119
|
+
decodeOptions.inSampleSize = sampleSize;
|
|
120
|
+
decodeOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
|
|
121
|
+
|
|
122
|
+
Bitmap raw;
|
|
123
|
+
try (InputStream stream = context.getContentResolver().openInputStream(uri)) {
|
|
124
|
+
if (stream == null) throw new IllegalStateException("Unable to open image.");
|
|
125
|
+
raw = BitmapFactory.decodeStream(stream, null, decodeOptions);
|
|
126
|
+
}
|
|
127
|
+
if (raw == null) throw new IllegalStateException("Unable to decode image.");
|
|
128
|
+
|
|
129
|
+
int orientation = ExifInterface.ORIENTATION_NORMAL;
|
|
130
|
+
try (InputStream stream = context.getContentResolver().openInputStream(uri)) {
|
|
131
|
+
if (stream != null) {
|
|
132
|
+
orientation = new ExifInterface(stream).getAttributeInt(
|
|
133
|
+
ExifInterface.TAG_ORIENTATION,
|
|
134
|
+
ExifInterface.ORIENTATION_NORMAL
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
} catch (Exception ignored) { }
|
|
138
|
+
|
|
139
|
+
Matrix matrix = orientationMatrix(orientation);
|
|
140
|
+
Bitmap oriented = Bitmap.createBitmap(
|
|
141
|
+
raw, 0, 0, raw.getWidth(), raw.getHeight(), matrix, true
|
|
142
|
+
);
|
|
143
|
+
if (oriented != raw) raw.recycle();
|
|
144
|
+
|
|
145
|
+
int largest = Math.max(oriented.getWidth(), oriented.getHeight());
|
|
146
|
+
if (largest <= maxDimension) return oriented;
|
|
147
|
+
float scale = maxDimension / (float) largest;
|
|
148
|
+
Bitmap resized = Bitmap.createScaledBitmap(
|
|
149
|
+
oriented,
|
|
150
|
+
Math.max(1, Math.round(oriented.getWidth() * scale)),
|
|
151
|
+
Math.max(1, Math.round(oriented.getHeight() * scale)),
|
|
152
|
+
true
|
|
153
|
+
);
|
|
154
|
+
if (resized != oriented) oriented.recycle();
|
|
155
|
+
return resized;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private static Matrix orientationMatrix(int orientation) {
|
|
159
|
+
Matrix matrix = new Matrix();
|
|
160
|
+
switch (orientation) {
|
|
161
|
+
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
|
|
162
|
+
matrix.postScale(-1f, 1f);
|
|
163
|
+
break;
|
|
164
|
+
case ExifInterface.ORIENTATION_ROTATE_180:
|
|
165
|
+
matrix.postRotate(180f);
|
|
166
|
+
break;
|
|
167
|
+
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
|
|
168
|
+
matrix.postScale(1f, -1f);
|
|
169
|
+
break;
|
|
170
|
+
case ExifInterface.ORIENTATION_TRANSPOSE:
|
|
171
|
+
matrix.postRotate(90f);
|
|
172
|
+
matrix.postScale(-1f, 1f);
|
|
173
|
+
break;
|
|
174
|
+
case ExifInterface.ORIENTATION_ROTATE_90:
|
|
175
|
+
matrix.postRotate(90f);
|
|
176
|
+
break;
|
|
177
|
+
case ExifInterface.ORIENTATION_TRANSVERSE:
|
|
178
|
+
matrix.postRotate(-90f);
|
|
179
|
+
matrix.postScale(-1f, 1f);
|
|
180
|
+
break;
|
|
181
|
+
case ExifInterface.ORIENTATION_ROTATE_270:
|
|
182
|
+
matrix.postRotate(-90f);
|
|
183
|
+
break;
|
|
184
|
+
default:
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
return matrix;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -106,10 +106,28 @@ def readModuleMode = { String variableName ->
|
|
|
106
106
|
def faceMode = readModuleMode('ANDROID_MLKIT_FACE_MODE')
|
|
107
107
|
def barcodeMode = readModuleMode('ANDROID_MLKIT_BARCODE_MODE')
|
|
108
108
|
def ocrMode = readModuleMode('ANDROID_MLKIT_OCR_MODE')
|
|
109
|
+
def imageLabelingMode = readModuleMode('ANDROID_MLKIT_IMAGE_LABELING_MODE')
|
|
110
|
+
def rawSubjectSegmentationMode = mlKitSuiteReadPluginVariable(
|
|
111
|
+
'ANDROID_MLKIT_SUBJECT_SEGMENTATION_MODE', 'inherit'
|
|
112
|
+
)
|
|
113
|
+
def subjectSegmentationMode = rawSubjectSegmentationMode == null
|
|
114
|
+
|| rawSubjectSegmentationMode.trim().isEmpty()
|
|
115
|
+
|| rawSubjectSegmentationMode.equalsIgnoreCase('inherit')
|
|
116
|
+
? (defaultMode == 'bundled' ? 'install-time' : defaultMode)
|
|
117
|
+
: mlKitSuiteNormalizeMode(
|
|
118
|
+
rawSubjectSegmentationMode, 'ANDROID_MLKIT_SUBJECT_SEGMENTATION_MODE'
|
|
119
|
+
)
|
|
120
|
+
if (subjectSegmentationMode == 'bundled') {
|
|
121
|
+
throw new GradleException(
|
|
122
|
+
'cordova-plugin-mlkit-suite: Subject Segmentation only supports install-time or first-use.'
|
|
123
|
+
)
|
|
124
|
+
}
|
|
109
125
|
def installTimeModules = []
|
|
110
126
|
if (faceMode == 'install-time') installTimeModules.add('face')
|
|
111
127
|
if (barcodeMode == 'install-time') installTimeModules.add('barcode')
|
|
112
128
|
if (ocrMode == 'install-time') installTimeModules.add('ocr')
|
|
129
|
+
if (imageLabelingMode == 'install-time') installTimeModules.add('ica')
|
|
130
|
+
if (subjectSegmentationMode == 'install-time') installTimeModules.add('subject_segment')
|
|
113
131
|
|
|
114
132
|
if (cordovaConfig.MIN_SDK_VERSION < 24) {
|
|
115
133
|
cordovaConfig.MIN_SDK_VERSION = 24
|
|
@@ -135,6 +153,9 @@ logger.lifecycle(
|
|
|
135
153
|
"Face ${faceMode} (${faceMode == 'bundled' ? mlKitFaceVersion : '17.1.0'}), " +
|
|
136
154
|
"Barcode ${barcodeMode} (${barcodeMode == 'bundled' ? '17.2.0' : '18.3.1'}), " +
|
|
137
155
|
"OCR ${ocrMode} (${ocrMode == 'bundled' ? '16.0.1' : '19.0.1'}), " +
|
|
156
|
+
"Image Labeling ${imageLabelingMode} (${imageLabelingMode == 'bundled' ? '17.0.9' : '16.0.8'}), " +
|
|
157
|
+
"Selfie Segmentation bundled (16.0.0-beta6), " +
|
|
158
|
+
"Subject Segmentation ${subjectSegmentationMode} (16.0.0-beta1), " +
|
|
138
159
|
'Document Scanner 16.0.0'
|
|
139
160
|
)
|
|
140
161
|
|
|
@@ -143,6 +164,8 @@ android {
|
|
|
143
164
|
manifestPlaceholders.mlKitSuiteFaceMode = faceMode
|
|
144
165
|
manifestPlaceholders.mlKitSuiteBarcodeMode = barcodeMode
|
|
145
166
|
manifestPlaceholders.mlKitSuiteOcrMode = ocrMode
|
|
167
|
+
manifestPlaceholders.mlKitSuiteImageLabelingMode = imageLabelingMode
|
|
168
|
+
manifestPlaceholders.mlKitSuiteSubjectSegmentationMode = subjectSegmentationMode
|
|
146
169
|
if (!installTimeModules.isEmpty()) {
|
|
147
170
|
manifestPlaceholders.mlKitSuiteDependencyMetadataName =
|
|
148
171
|
'com.google.mlkit.vision.DEPENDENCIES'
|
|
@@ -181,7 +204,14 @@ dependencies {
|
|
|
181
204
|
} else {
|
|
182
205
|
implementation 'com.google.android.gms:play-services-mlkit-text-recognition:19.0.1'
|
|
183
206
|
}
|
|
207
|
+
if (imageLabelingMode == 'bundled') {
|
|
208
|
+
implementation 'com.google.mlkit:image-labeling:17.0.9'
|
|
209
|
+
} else {
|
|
210
|
+
implementation 'com.google.android.gms:play-services-mlkit-image-labeling:16.0.8'
|
|
211
|
+
}
|
|
184
212
|
implementation 'com.google.android.gms:play-services-mlkit-document-scanner:16.0.0'
|
|
213
|
+
implementation 'com.google.mlkit:segmentation-selfie:16.0.0-beta6'
|
|
214
|
+
implementation 'com.google.android.gms:play-services-mlkit-subject-segmentation:16.0.0-beta1'
|
|
185
215
|
compileOnly "androidx.webkit:webkit:${webKitByCordova[cordovaAndroidMajor]}"
|
|
186
216
|
}
|
|
187
217
|
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
<string name="mlkit_suite_barcode_searching">Looking for a barcode</string>
|
|
44
44
|
<string name="mlkit_suite_barcode_center">Center the barcode inside the frame</string>
|
|
45
45
|
<string name="mlkit_suite_barcode_detected">Detected %1$s — hold steady (%2$d/%3$d)</string>
|
|
46
|
+
<string name="mlkit_suite_barcode_success">Code detected</string>
|
|
46
47
|
<string name="mlkit_suite_barcode_analyze_failed">Unable to analyze the camera image</string>
|
|
47
48
|
<string name="mlkit_suite_barcode_timeout">No barcode was confirmed before the timeout.</string>
|
|
48
49
|
<string name="mlkit_suite_barcode_cancelled">Barcode scanning was cancelled by the user.</string>
|
|
@@ -68,6 +69,13 @@
|
|
|
68
69
|
<string name="mlkit_suite_image_open_failed">Unable to open imagePath.</string>
|
|
69
70
|
<string name="mlkit_suite_text_recognition_failed">Unable to recognize text in the image.</string>
|
|
70
71
|
<string name="mlkit_suite_barcode_image_failed">Unable to scan barcodes in the image.</string>
|
|
72
|
+
<string name="mlkit_suite_image_labeling_failed">Unable to label the image.</string>
|
|
73
|
+
<string name="mlkit_suite_selfie_segmentation_failed">Unable to segment the person in the image.</string>
|
|
74
|
+
<string name="mlkit_suite_subject_segmentation_failed">Unable to segment the subjects in the image.</string>
|
|
75
|
+
<string name="mlkit_suite_subject_check_failed">Unable to check the Subject Segmentation module.</string>
|
|
76
|
+
<string name="mlkit_suite_subject_download_cancelled">Subject Segmentation module download was cancelled.</string>
|
|
77
|
+
<string name="mlkit_suite_subject_download_failed">Unable to download the Subject Segmentation module (%1$s).</string>
|
|
78
|
+
<string name="mlkit_suite_subject_download_start_failed">Unable to start the Subject Segmentation module download.</string>
|
|
71
79
|
<string name="mlkit_suite_camera_permission_denied">Camera permission was denied.</string>
|
|
72
80
|
<string name="mlkit_suite_busy">Another interactive ML Kit session is already active.</string>
|
|
73
81
|
</resources>
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
<string name="mlkit_suite_barcode_searching">Buscando un código</string>
|
|
44
44
|
<string name="mlkit_suite_barcode_center">Centrá el código dentro del recuadro</string>
|
|
45
45
|
<string name="mlkit_suite_barcode_detected">%1$s detectado — mantené firme (%2$d/%3$d)</string>
|
|
46
|
+
<string name="mlkit_suite_barcode_success">Código detectado</string>
|
|
46
47
|
<string name="mlkit_suite_barcode_analyze_failed">No se pudo analizar la imagen de la cámara</string>
|
|
47
48
|
<string name="mlkit_suite_barcode_timeout">No se confirmó ningún código antes de agotar el tiempo.</string>
|
|
48
49
|
<string name="mlkit_suite_barcode_cancelled">El usuario canceló el escaneo de códigos.</string>
|
|
@@ -68,6 +69,13 @@
|
|
|
68
69
|
<string name="mlkit_suite_image_open_failed">No se pudo abrir imagePath.</string>
|
|
69
70
|
<string name="mlkit_suite_text_recognition_failed">No se pudo reconocer texto en la imagen.</string>
|
|
70
71
|
<string name="mlkit_suite_barcode_image_failed">No se pudieron detectar códigos en la imagen.</string>
|
|
72
|
+
<string name="mlkit_suite_image_labeling_failed">No se pudo clasificar la imagen.</string>
|
|
73
|
+
<string name="mlkit_suite_selfie_segmentation_failed">No se pudo segmentar la persona en la imagen.</string>
|
|
74
|
+
<string name="mlkit_suite_subject_segmentation_failed">No se pudieron segmentar los sujetos de la imagen.</string>
|
|
75
|
+
<string name="mlkit_suite_subject_check_failed">No se pudo verificar el módulo de segmentación de sujetos.</string>
|
|
76
|
+
<string name="mlkit_suite_subject_download_cancelled">Se canceló la descarga del módulo de segmentación de sujetos.</string>
|
|
77
|
+
<string name="mlkit_suite_subject_download_failed">No se pudo descargar el módulo de segmentación de sujetos (%1$s).</string>
|
|
78
|
+
<string name="mlkit_suite_subject_download_start_failed">No se pudo iniciar la descarga del módulo de segmentación de sujetos.</string>
|
|
71
79
|
<string name="mlkit_suite_camera_permission_denied">Se denegó el permiso de cámara.</string>
|
|
72
80
|
<string name="mlkit_suite_busy">Ya hay otra sesión interactiva de ML Kit activa.</string>
|
|
73
81
|
</resources>
|
|
@@ -5,6 +5,12 @@ final class BarcodeOverlayView: UIView {
|
|
|
5
5
|
private let guideColor: UIColor?
|
|
6
6
|
private var message: String
|
|
7
7
|
private var candidateVisible = false
|
|
8
|
+
private var successVisible = false
|
|
9
|
+
private var displayedGuide: CGRect?
|
|
10
|
+
private var animationStart = CGRect.zero
|
|
11
|
+
private var animationTarget = CGRect.zero
|
|
12
|
+
private var animationStartedAt: CFTimeInterval = 0
|
|
13
|
+
private var displayLink: CADisplayLink?
|
|
8
14
|
|
|
9
15
|
init(instruction: String, guideColor: UIColor?, initialMessage: String) {
|
|
10
16
|
self.instruction = instruction
|
|
@@ -27,23 +33,60 @@ final class BarcodeOverlayView: UIView {
|
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
func contains(barcodeFrame: CGRect, imageSize: CGSize) -> Bool {
|
|
36
|
+
guard let mapped = mappedFrame(barcodeFrame, imageSize: imageSize) else { return false }
|
|
37
|
+
return guideRect(in: bounds).contains(mapped)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func showSuccess(barcodeFrame: CGRect, imageSize: CGSize, message: String) {
|
|
41
|
+
let initial = guideRect(in: bounds)
|
|
42
|
+
let mapped = mappedFrame(barcodeFrame, imageSize: imageSize) ?? initial
|
|
43
|
+
let padded = mapped.insetBy(dx: -10, dy: -10).intersection(bounds)
|
|
44
|
+
animationStart = initial
|
|
45
|
+
animationTarget = padded.isNull || padded.isEmpty ? initial : padded
|
|
46
|
+
displayedGuide = initial
|
|
47
|
+
self.message = message
|
|
48
|
+
candidateVisible = true
|
|
49
|
+
successVisible = true
|
|
50
|
+
displayLink?.invalidate()
|
|
51
|
+
animationStartedAt = CACurrentMediaTime()
|
|
52
|
+
let link = CADisplayLink(target: self, selector: #selector(advanceSuccessAnimation(_:)))
|
|
53
|
+
link.add(to: .main, forMode: .common)
|
|
54
|
+
displayLink = link
|
|
55
|
+
setNeedsDisplay()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private func mappedFrame(_ barcodeFrame: CGRect, imageSize: CGSize) -> CGRect? {
|
|
30
59
|
guard imageSize.width > 0, imageSize.height > 0, !barcodeFrame.isNull,
|
|
31
|
-
!barcodeFrame.isEmpty else { return
|
|
32
|
-
let guide = guideRect(in: bounds)
|
|
60
|
+
!barcodeFrame.isEmpty else { return nil }
|
|
33
61
|
let scale = max(bounds.width / imageSize.width, bounds.height / imageSize.height)
|
|
34
62
|
let offsetX = (bounds.width - imageSize.width * scale) / 2
|
|
35
63
|
let offsetY = (bounds.height - imageSize.height * scale) / 2
|
|
36
|
-
|
|
64
|
+
return CGRect(
|
|
37
65
|
x: barcodeFrame.minX * scale + offsetX,
|
|
38
66
|
y: barcodeFrame.minY * scale + offsetY,
|
|
39
67
|
width: barcodeFrame.width * scale,
|
|
40
68
|
height: barcodeFrame.height * scale
|
|
41
69
|
)
|
|
42
|
-
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@objc private func advanceSuccessAnimation(_ link: CADisplayLink) {
|
|
73
|
+
let progress = min(1, max(0, (CACurrentMediaTime() - animationStartedAt) / 0.22))
|
|
74
|
+
let eased = 1 - pow(1 - CGFloat(progress), 3)
|
|
75
|
+
displayedGuide = CGRect(
|
|
76
|
+
x: interpolate(animationStart.minX, animationTarget.minX, eased),
|
|
77
|
+
y: interpolate(animationStart.minY, animationTarget.minY, eased),
|
|
78
|
+
width: interpolate(animationStart.width, animationTarget.width, eased),
|
|
79
|
+
height: interpolate(animationStart.height, animationTarget.height, eased)
|
|
80
|
+
)
|
|
81
|
+
setNeedsDisplay()
|
|
82
|
+
if progress >= 1 {
|
|
83
|
+
link.invalidate()
|
|
84
|
+
displayLink = nil
|
|
85
|
+
}
|
|
43
86
|
}
|
|
44
87
|
|
|
45
88
|
override func draw(_ rect: CGRect) {
|
|
46
|
-
let guide = guideRect(in: rect)
|
|
89
|
+
let guide = successVisible ? (displayedGuide ?? guideRect(in: rect)) : guideRect(in: rect)
|
|
47
90
|
|
|
48
91
|
let shade = UIBezierPath(rect: rect)
|
|
49
92
|
shade.append(UIBezierPath(roundedRect: guide, cornerRadius: 18))
|
|
@@ -67,7 +110,7 @@ final class BarcodeOverlayView: UIView {
|
|
|
67
110
|
])
|
|
68
111
|
}
|
|
69
112
|
|
|
70
|
-
if !instruction.isEmpty {
|
|
113
|
+
if !successVisible && !instruction.isEmpty {
|
|
71
114
|
drawCentered(
|
|
72
115
|
instruction,
|
|
73
116
|
in: CGRect(x: 20, y: max(42, guide.minY - 64), width: rect.width - 40, height: 48),
|
|
@@ -139,4 +182,12 @@ final class BarcodeOverlayView: UIView {
|
|
|
139
182
|
]
|
|
140
183
|
)
|
|
141
184
|
}
|
|
185
|
+
|
|
186
|
+
private func interpolate(_ start: CGFloat, _ end: CGFloat, _ progress: CGFloat) -> CGFloat {
|
|
187
|
+
start + (end - start) * progress
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
deinit {
|
|
191
|
+
displayLink?.invalidate()
|
|
192
|
+
}
|
|
142
193
|
}
|
|
@@ -81,6 +81,7 @@ struct BarcodeScanOptions {
|
|
|
81
81
|
let formats: BarcodeFormat
|
|
82
82
|
let showCloseButton: Bool
|
|
83
83
|
let showTorchButton: Bool
|
|
84
|
+
let beepEnabled: Bool
|
|
84
85
|
let instructionText: String
|
|
85
86
|
let localization: MlKitLocalization
|
|
86
87
|
let guideColor: UIColor?
|
|
@@ -90,7 +91,7 @@ struct BarcodeScanOptions {
|
|
|
90
91
|
localization = try MlKitLocalization(dictionary: dictionary)
|
|
91
92
|
let known = Set([
|
|
92
93
|
"formats", "showCloseButton", "showTorchButton", "instructionText",
|
|
93
|
-
"guideColor", "timeoutMs", "moduleLoadingMessage", "locale"
|
|
94
|
+
"guideColor", "beepEnabled", "timeoutMs", "moduleLoadingMessage", "locale"
|
|
94
95
|
])
|
|
95
96
|
for key in dictionary.keys where !known.contains(key) {
|
|
96
97
|
throw MlKitSuiteError.invalidArgument("Unknown barcode scan option: \(key).")
|
|
@@ -124,6 +125,7 @@ struct BarcodeScanOptions {
|
|
|
124
125
|
formats = try BarcodeFormatSupport.parse(dictionary["formats"])
|
|
125
126
|
showCloseButton = try boolean("showCloseButton", fallback: true)
|
|
126
127
|
showTorchButton = try boolean("showTorchButton", fallback: true)
|
|
128
|
+
beepEnabled = try boolean("beepEnabled", fallback: true)
|
|
127
129
|
timeoutMs = try integer("timeoutMs", fallback: 0, minimum: 0, maximum: 600_000)
|
|
128
130
|
if let raw = dictionary["instructionText"], !(raw is NSNull) {
|
|
129
131
|
guard let string = raw as? String else {
|