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.
Files changed (38) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +526 -640
  3. package/docs/screenshots/ios-text-recognition.jpg +0 -0
  4. package/package.json +7 -3
  5. package/plugin.xml +149 -134
  6. package/src/android/java/BarcodeFrameAnalyzer.java +115 -111
  7. package/src/android/java/BarcodeOverlayView.java +83 -25
  8. package/src/android/java/BarcodeScanActivity.java +100 -12
  9. package/src/android/java/BarcodeScanConfig.java +6 -1
  10. package/src/android/java/BarcodeScanRegion.java +26 -8
  11. package/src/android/java/MlKitModelsModuleInstaller.java +35 -16
  12. package/src/android/java/MlKitSuitePlugin.java +315 -33
  13. package/src/android/java/SelfieSegmentationProcessor.java +215 -0
  14. package/src/android/java/StaticImageProcessor.java +208 -151
  15. package/src/android/java/SubjectSegmentationModuleInstaller.java +154 -0
  16. package/src/android/java/SubjectSegmentationProcessor.java +189 -0
  17. package/src/android/mlkit-suite.gradle +30 -0
  18. package/src/android/res/values/mlkit_suite_strings.xml +8 -0
  19. package/src/android/res/values-es/mlkit_suite_strings.xml +8 -0
  20. package/src/ios/BarcodeOverlayView.swift +57 -6
  21. package/src/ios/BarcodeScanTypes.swift +3 -1
  22. package/src/ios/BarcodeScanViewController.swift +120 -5
  23. package/src/ios/MlKitSuitePlugin.swift +222 -13
  24. package/src/ios/MlKitSuiteResources.bundle/en.lproj/Localizable.strings +4 -0
  25. package/src/ios/MlKitSuiteResources.bundle/es.lproj/Localizable.strings +4 -0
  26. package/src/ios/SelfieSegmentationProcessor.swift +105 -0
  27. package/src/ios/TextRecognitionProcessor.swift +99 -0
  28. package/tests/ios-image-labeling.test.js +17 -0
  29. package/tests/ios-live-barcode.test.js +5 -0
  30. package/tests/ios-selfie-segmentation.test.js +24 -0
  31. package/tests/ios-static-text.test.js +21 -0
  32. package/tests/java/BarcodeScanRegionTest.java +12 -0
  33. package/tests/java-confirmation.test.js +41 -41
  34. package/tests/js-api.test.js +42 -2
  35. package/www/MlKit.js +111 -7
  36. package/docs/screenshots/capabilities.jpg +0 -0
  37. package/docs/screenshots/demo-tour.gif +0 -0
  38. 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 false }
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
- let mapped = CGRect(
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
- return guide.contains(mapped)
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 {