capacitor-camera-multicapture 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CameraMultiCapture.podspec +17 -0
- package/LICENSE +21 -0
- package/Package.swift +28 -0
- package/README.md +551 -0
- package/android/build.gradle +69 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/CameraConfig.java +45 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/CameraConfigMapper.java +98 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/CameraMultiCapture.java +10 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/CameraMultiCapturePlugin.java +1340 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/ExifWrapper.java +108 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/GenericUploadWorker.java +173 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/ImageUtils.java +215 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/PluginVersion.java +6 -0
- package/android/src/main/java/dev/hemang/cameramulticapture/ThumbnailGenerator.java +48 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/esm/controllers/camera-controller.d.ts +127 -0
- package/dist/esm/controllers/camera-controller.js +388 -0
- package/dist/esm/controllers/camera-controller.js.map +1 -0
- package/dist/esm/controllers/gallery-controller.d.ts +66 -0
- package/dist/esm/controllers/gallery-controller.js +333 -0
- package/dist/esm/controllers/gallery-controller.js.map +1 -0
- package/dist/esm/definitions.d.ts +372 -0
- package/dist/esm/definitions.js +6 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/overlay-manager.d.ts +124 -0
- package/dist/esm/overlay-manager.js +732 -0
- package/dist/esm/overlay-manager.js.map +1 -0
- package/dist/esm/overlay.d.ts +2 -0
- package/dist/esm/overlay.js +39 -0
- package/dist/esm/overlay.js.map +1 -0
- package/dist/esm/registration.d.ts +5 -0
- package/dist/esm/registration.js +8 -0
- package/dist/esm/registration.js.map +1 -0
- package/dist/esm/types/ui-types.d.ts +78 -0
- package/dist/esm/types/ui-types.js +2 -0
- package/dist/esm/types/ui-types.js.map +1 -0
- package/dist/esm/ui/capture-gesture-handler.d.ts +15 -0
- package/dist/esm/ui/capture-gesture-handler.js +87 -0
- package/dist/esm/ui/capture-gesture-handler.js.map +1 -0
- package/dist/esm/ui/default-styles.d.ts +27 -0
- package/dist/esm/ui/default-styles.js +134 -0
- package/dist/esm/ui/default-styles.js.map +1 -0
- package/dist/esm/ui/image-editor.d.ts +10 -0
- package/dist/esm/ui/image-editor.js +103 -0
- package/dist/esm/ui/image-editor.js.map +1 -0
- package/dist/esm/ui/layout-manager.d.ts +42 -0
- package/dist/esm/ui/layout-manager.js +199 -0
- package/dist/esm/ui/layout-manager.js.map +1 -0
- package/dist/esm/ui/media-viewer.d.ts +2 -0
- package/dist/esm/ui/media-viewer.js +267 -0
- package/dist/esm/ui/media-viewer.js.map +1 -0
- package/dist/esm/ui/pinch-zoom-handler.d.ts +37 -0
- package/dist/esm/ui/pinch-zoom-handler.js +118 -0
- package/dist/esm/ui/pinch-zoom-handler.js.map +1 -0
- package/dist/esm/ui/ui-factory.d.ts +39 -0
- package/dist/esm/ui/ui-factory.js +272 -0
- package/dist/esm/ui/ui-factory.js.map +1 -0
- package/dist/esm/version.d.ts +1 -0
- package/dist/esm/version.js +3 -0
- package/dist/esm/version.js.map +1 -0
- package/dist/esm/web.d.ts +69 -0
- package/dist/esm/web.js +122 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +6274 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +6276 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/CameraMultiCapturePlugin/CameraMultiCapture.swift +8 -0
- package/ios/Sources/CameraMultiCapturePlugin/CameraMultiCapturePlugin.swift +1551 -0
- package/ios/Sources/CameraMultiCapturePlugin/PluginVersion.swift +2 -0
- package/ios/Tests/CameraMultiCapturePluginTests/CameraMultiCapturePluginTests.swift +15 -0
- package/package.json +93 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
package dev.hemang.cameramulticapture;
|
|
2
|
+
|
|
3
|
+
import androidx.exifinterface.media.ExifInterface;
|
|
4
|
+
import android.util.Log;
|
|
5
|
+
|
|
6
|
+
import java.io.IOException;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Adapted from Capacitor Camera Plugin
|
|
10
|
+
* Source: https://github.com/ionic-team/capacitor-plugins/blob/main/camera/android/src/main/java/com/capacitorjs/plugins/camera/ExifWrapper.java
|
|
11
|
+
* Copyright 2020-present Ionic (https://ionic.io)
|
|
12
|
+
* Licensed under MIT License
|
|
13
|
+
*/
|
|
14
|
+
public class ExifWrapper {
|
|
15
|
+
private static final String TAG = "ExifWrapper";
|
|
16
|
+
private final ExifInterface exif;
|
|
17
|
+
|
|
18
|
+
private final String[] attributes = new String[] {
|
|
19
|
+
ExifInterface.TAG_APERTURE_VALUE,
|
|
20
|
+
ExifInterface.TAG_ARTIST,
|
|
21
|
+
ExifInterface.TAG_DATETIME,
|
|
22
|
+
ExifInterface.TAG_DATETIME_DIGITIZED,
|
|
23
|
+
ExifInterface.TAG_DATETIME_ORIGINAL,
|
|
24
|
+
ExifInterface.TAG_EXPOSURE_TIME,
|
|
25
|
+
ExifInterface.TAG_FLASH,
|
|
26
|
+
ExifInterface.TAG_FOCAL_LENGTH,
|
|
27
|
+
ExifInterface.TAG_GPS_ALTITUDE,
|
|
28
|
+
ExifInterface.TAG_GPS_ALTITUDE_REF,
|
|
29
|
+
ExifInterface.TAG_GPS_DATESTAMP,
|
|
30
|
+
ExifInterface.TAG_GPS_LATITUDE,
|
|
31
|
+
ExifInterface.TAG_GPS_LATITUDE_REF,
|
|
32
|
+
ExifInterface.TAG_GPS_LONGITUDE,
|
|
33
|
+
ExifInterface.TAG_GPS_LONGITUDE_REF,
|
|
34
|
+
ExifInterface.TAG_GPS_PROCESSING_METHOD,
|
|
35
|
+
ExifInterface.TAG_GPS_TIMESTAMP,
|
|
36
|
+
ExifInterface.TAG_IMAGE_LENGTH,
|
|
37
|
+
ExifInterface.TAG_IMAGE_WIDTH,
|
|
38
|
+
ExifInterface.TAG_ISO_SPEED,
|
|
39
|
+
ExifInterface.TAG_MAKE,
|
|
40
|
+
ExifInterface.TAG_MODEL,
|
|
41
|
+
ExifInterface.TAG_ORIENTATION,
|
|
42
|
+
ExifInterface.TAG_SUBSEC_TIME,
|
|
43
|
+
ExifInterface.TAG_SUBSEC_TIME_DIGITIZED,
|
|
44
|
+
ExifInterface.TAG_SUBSEC_TIME_ORIGINAL,
|
|
45
|
+
ExifInterface.TAG_WHITE_BALANCE
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
public ExifWrapper(ExifInterface exif) {
|
|
49
|
+
this.exif = exif;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public ExifWrapper(String filePath) throws IOException {
|
|
53
|
+
this.exif = new ExifInterface(filePath);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Copy EXIF data from this wrapper to a destination file
|
|
58
|
+
* @param destFile Path to destination file
|
|
59
|
+
*/
|
|
60
|
+
public void copyExif(String destFile) {
|
|
61
|
+
if (exif == null) {
|
|
62
|
+
Log.w(TAG, "Source EXIF is null, cannot copy");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
ExifInterface destExif = new ExifInterface(destFile);
|
|
68
|
+
for (String attribute : attributes) {
|
|
69
|
+
String value = exif.getAttribute(attribute);
|
|
70
|
+
if (value != null) {
|
|
71
|
+
destExif.setAttribute(attribute, value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
destExif.saveAttributes();
|
|
75
|
+
Log.d(TAG, "EXIF data copied successfully to " + destFile);
|
|
76
|
+
} catch (IOException e) {
|
|
77
|
+
Log.e(TAG, "Failed to copy EXIF data: " + e.getMessage(), e);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reset orientation to normal (1) after physical rotation
|
|
83
|
+
*/
|
|
84
|
+
public void resetOrientation() {
|
|
85
|
+
if (exif != null) {
|
|
86
|
+
try {
|
|
87
|
+
exif.setAttribute(ExifInterface.TAG_ORIENTATION,
|
|
88
|
+
String.valueOf(ExifInterface.ORIENTATION_NORMAL));
|
|
89
|
+
exif.saveAttributes();
|
|
90
|
+
Log.d(TAG, "EXIF orientation reset to NORMAL");
|
|
91
|
+
} catch (IOException e) {
|
|
92
|
+
Log.e(TAG, "Failed to reset orientation: " + e.getMessage(), e);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the current orientation value
|
|
99
|
+
* @return EXIF orientation value
|
|
100
|
+
*/
|
|
101
|
+
public int getOrientation() {
|
|
102
|
+
if (exif == null) {
|
|
103
|
+
return ExifInterface.ORIENTATION_NORMAL;
|
|
104
|
+
}
|
|
105
|
+
return exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
package dev.hemang.cameramulticapture;
|
|
2
|
+
|
|
3
|
+
import android.content.Context;
|
|
4
|
+
import android.net.Uri;
|
|
5
|
+
import android.util.Log;
|
|
6
|
+
import androidx.annotation.NonNull;
|
|
7
|
+
import androidx.work.Data;
|
|
8
|
+
import androidx.work.Worker;
|
|
9
|
+
import androidx.work.WorkerParameters;
|
|
10
|
+
import com.getcapacitor.JSObject;
|
|
11
|
+
import org.json.JSONObject;
|
|
12
|
+
import java.io.File;
|
|
13
|
+
import java.io.IOException;
|
|
14
|
+
import java.util.Iterator;
|
|
15
|
+
import java.util.concurrent.TimeUnit;
|
|
16
|
+
import okhttp3.*;
|
|
17
|
+
|
|
18
|
+
public class GenericUploadWorker extends Worker {
|
|
19
|
+
private static final String TAG = "GenericUploadWorker";
|
|
20
|
+
private static final MediaType MEDIA_TYPE_JPEG = MediaType.parse("image/jpeg");
|
|
21
|
+
|
|
22
|
+
private static class UploadResult {
|
|
23
|
+
boolean success;
|
|
24
|
+
String errorMessage;
|
|
25
|
+
|
|
26
|
+
UploadResult(boolean success, String errorMessage) {
|
|
27
|
+
this.success = success;
|
|
28
|
+
this.errorMessage = errorMessage;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public GenericUploadWorker(@NonNull Context context, @NonNull WorkerParameters params) {
|
|
33
|
+
super(context, params);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@NonNull
|
|
37
|
+
@Override
|
|
38
|
+
public Result doWork() {
|
|
39
|
+
try {
|
|
40
|
+
String jobId = getInputData().getString("jobId");
|
|
41
|
+
String imageUri = getInputData().getString("imageUri");
|
|
42
|
+
String uploadEndpoint = getInputData().getString("uploadEndpoint");
|
|
43
|
+
String headersJson = getInputData().getString("headers");
|
|
44
|
+
String formDataJson = getInputData().getString("formData");
|
|
45
|
+
String method = getInputData().getString("method");
|
|
46
|
+
String fileName = getInputData().getString("fileName");
|
|
47
|
+
boolean deleteAfterUpload = getInputData().getBoolean("deleteAfterUpload", true);
|
|
48
|
+
|
|
49
|
+
Log.d(TAG, "Starting upload job: " + jobId);
|
|
50
|
+
|
|
51
|
+
UploadResult result = performHttpUpload(imageUri, uploadEndpoint, headersJson, formDataJson, method, fileName);
|
|
52
|
+
|
|
53
|
+
if (result.success) {
|
|
54
|
+
Log.d(TAG, "Upload completed successfully: " + jobId);
|
|
55
|
+
|
|
56
|
+
if (deleteAfterUpload) {
|
|
57
|
+
try {
|
|
58
|
+
File imageFile = new File(Uri.parse(imageUri).getPath());
|
|
59
|
+
if (imageFile.exists() && imageFile.delete()) {
|
|
60
|
+
Log.d(TAG, "✅ Cleaned up file after successful upload: " + imageUri);
|
|
61
|
+
}
|
|
62
|
+
} catch (Exception e) {
|
|
63
|
+
Log.w(TAG, "Failed to clean up file: " + e.getMessage());
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return Result.success();
|
|
68
|
+
} else {
|
|
69
|
+
Log.e(TAG, "Upload failed: " + jobId + " - " + result.errorMessage);
|
|
70
|
+
Data errorData = new Data.Builder()
|
|
71
|
+
.putString("error", result.errorMessage != null ? result.errorMessage : "Upload failed")
|
|
72
|
+
.build();
|
|
73
|
+
return Result.failure(errorData);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
} catch (Exception e) {
|
|
77
|
+
Log.e(TAG, "Upload error: " + e.getMessage(), e);
|
|
78
|
+
Data errorData = new Data.Builder()
|
|
79
|
+
.putString("error", e.getMessage())
|
|
80
|
+
.build();
|
|
81
|
+
return Result.retry();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private UploadResult performHttpUpload(String imageUri, String endpoint, String headersJson,
|
|
86
|
+
String formDataJson, String method, String fileName) {
|
|
87
|
+
try {
|
|
88
|
+
OkHttpClient client = new OkHttpClient.Builder()
|
|
89
|
+
.connectTimeout(45, TimeUnit.SECONDS)
|
|
90
|
+
.writeTimeout(120, TimeUnit.SECONDS)
|
|
91
|
+
.readTimeout(90, TimeUnit.SECONDS)
|
|
92
|
+
.retryOnConnectionFailure(true)
|
|
93
|
+
.connectionPool(new okhttp3.ConnectionPool(10, 5, TimeUnit.MINUTES))
|
|
94
|
+
.build();
|
|
95
|
+
|
|
96
|
+
File imageFile = new File(Uri.parse(imageUri).getPath());
|
|
97
|
+
if (!imageFile.exists()) {
|
|
98
|
+
Log.e(TAG, "Image file not found: " + imageUri);
|
|
99
|
+
return new UploadResult(false, "Image file not found: " + imageUri);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
JSONObject headers = new JSONObject(headersJson);
|
|
103
|
+
JSONObject formData = new JSONObject(formDataJson);
|
|
104
|
+
|
|
105
|
+
Request.Builder requestBuilder = new Request.Builder().url(endpoint);
|
|
106
|
+
|
|
107
|
+
Iterator<String> headerKeys = headers.keys();
|
|
108
|
+
while (headerKeys.hasNext()) {
|
|
109
|
+
String key = headerKeys.next();
|
|
110
|
+
String value = headers.getString(key);
|
|
111
|
+
requestBuilder.addHeader(key, value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
RequestBody requestBody;
|
|
115
|
+
if ("PUT".equalsIgnoreCase(method)) {
|
|
116
|
+
requestBody = RequestBody.create(MEDIA_TYPE_JPEG, imageFile);
|
|
117
|
+
} else {
|
|
118
|
+
// For POST requests, use multipart form
|
|
119
|
+
MultipartBody.Builder multipartBuilder = new MultipartBody.Builder()
|
|
120
|
+
.setType(MultipartBody.FORM);
|
|
121
|
+
|
|
122
|
+
Iterator<String> formKeys = formData.keys();
|
|
123
|
+
while (formKeys.hasNext()) {
|
|
124
|
+
String key = formKeys.next();
|
|
125
|
+
String value = formData.getString(key);
|
|
126
|
+
multipartBuilder.addFormDataPart(key, value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
String finalFileName = (fileName != null && !fileName.isEmpty()) ?
|
|
130
|
+
fileName : "photo_" + System.currentTimeMillis() + ".jpg";
|
|
131
|
+
multipartBuilder.addFormDataPart("file", finalFileName,
|
|
132
|
+
RequestBody.create(MEDIA_TYPE_JPEG, imageFile));
|
|
133
|
+
|
|
134
|
+
requestBody = multipartBuilder.build();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if ("PUT".equalsIgnoreCase(method)) {
|
|
138
|
+
requestBuilder.put(requestBody);
|
|
139
|
+
} else {
|
|
140
|
+
requestBuilder.post(requestBody);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
Request request = requestBuilder.build();
|
|
144
|
+
try (Response response = client.newCall(request).execute()) {
|
|
145
|
+
boolean success = response.isSuccessful();
|
|
146
|
+
Log.d(TAG, "Upload response: " + response.code() + " - " + response.message());
|
|
147
|
+
|
|
148
|
+
if (!success) {
|
|
149
|
+
String errorBody = response.body() != null ? response.body().string() : "No error details";
|
|
150
|
+
Log.e(TAG, "Upload failed with response: " + response.code() + " - " + errorBody);
|
|
151
|
+
return new UploadResult(false, "HTTP " + response.code() + ": " + response.message() + " - " + errorBody);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return new UploadResult(true, null);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
} catch (java.net.ConnectException e) {
|
|
158
|
+
String errorMsg = "Connection failed to Azure Blob Storage. This usually happens when uploading multiple files simultaneously. Try uploading fewer files at once.";
|
|
159
|
+
Log.e(TAG, errorMsg + " Details: " + e.getMessage(), e);
|
|
160
|
+
return new UploadResult(false, errorMsg);
|
|
161
|
+
|
|
162
|
+
} catch (java.net.SocketTimeoutException e) {
|
|
163
|
+
String errorMsg = "Upload timeout. The file may be too large or network is slow.";
|
|
164
|
+
Log.e(TAG, errorMsg + " Details: " + e.getMessage(), e);
|
|
165
|
+
return new UploadResult(false, errorMsg);
|
|
166
|
+
|
|
167
|
+
} catch (Exception e) {
|
|
168
|
+
String errorMsg = "Upload failed: " + e.getClass().getSimpleName() + " - " + e.getMessage();
|
|
169
|
+
Log.e(TAG, "HTTP upload error: " + errorMsg, e);
|
|
170
|
+
return new UploadResult(false, errorMsg);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
package dev.hemang.cameramulticapture;
|
|
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
|
+
import android.util.Log;
|
|
9
|
+
import androidx.exifinterface.media.ExifInterface;
|
|
10
|
+
|
|
11
|
+
import java.io.File;
|
|
12
|
+
import java.io.FileOutputStream;
|
|
13
|
+
import java.io.IOException;
|
|
14
|
+
import java.io.InputStream;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Image orientation correction utilities
|
|
18
|
+
* Adapted from Capacitor Camera Plugin
|
|
19
|
+
* Source: https://github.com/ionic-team/capacitor-plugins/blob/main/camera/android/src/main/java/com/capacitorjs/plugins/camera/ImageUtils.java
|
|
20
|
+
* Copyright 2020-present Ionic (https://ionic.io)
|
|
21
|
+
* Licensed under MIT License
|
|
22
|
+
*/
|
|
23
|
+
public class ImageUtils {
|
|
24
|
+
private static final String TAG = "ImageUtils";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Correct the orientation of an image file by reading EXIF, physically rotating, and resetting EXIF
|
|
28
|
+
* @param imageFile The image file to correct
|
|
29
|
+
* @return true if correction was successful or not needed, false if failed
|
|
30
|
+
*/
|
|
31
|
+
public static boolean correctImageOrientation(File imageFile) {
|
|
32
|
+
if (imageFile == null || !imageFile.exists()) {
|
|
33
|
+
Log.e(TAG, "Image file does not exist");
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
ExifWrapper exifWrapper = new ExifWrapper(imageFile.getAbsolutePath());
|
|
39
|
+
int orientation = exifWrapper.getOrientation();
|
|
40
|
+
|
|
41
|
+
if (orientation == ExifInterface.ORIENTATION_NORMAL ||
|
|
42
|
+
orientation == ExifInterface.ORIENTATION_UNDEFINED) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Bitmap bitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath());
|
|
47
|
+
if (bitmap == null) {
|
|
48
|
+
Log.e(TAG, "Failed to decode image file");
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Bitmap rotatedBitmap = rotateBitmapByExif(bitmap, orientation);
|
|
53
|
+
|
|
54
|
+
if (rotatedBitmap != bitmap) {
|
|
55
|
+
FileOutputStream out = new FileOutputStream(imageFile);
|
|
56
|
+
rotatedBitmap.compress(Bitmap.CompressFormat.JPEG, 95, out);
|
|
57
|
+
out.flush();
|
|
58
|
+
out.close();
|
|
59
|
+
|
|
60
|
+
bitmap.recycle();
|
|
61
|
+
rotatedBitmap.recycle();
|
|
62
|
+
|
|
63
|
+
ExifWrapper newExifWrapper = new ExifWrapper(imageFile.getAbsolutePath());
|
|
64
|
+
newExifWrapper.resetOrientation();
|
|
65
|
+
|
|
66
|
+
return true;
|
|
67
|
+
} else {
|
|
68
|
+
bitmap.recycle();
|
|
69
|
+
exifWrapper.resetOrientation();
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
} catch (Exception e) {
|
|
74
|
+
Log.e(TAG, "Error correcting image orientation: " + e.getMessage(), e);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Rotate bitmap according to EXIF orientation
|
|
81
|
+
* @param bitmap Source bitmap
|
|
82
|
+
* @param exifOrientation EXIF orientation value
|
|
83
|
+
* @return Rotated bitmap, or same bitmap if no rotation needed
|
|
84
|
+
*/
|
|
85
|
+
private static Bitmap rotateBitmapByExif(Bitmap bitmap, int exifOrientation) {
|
|
86
|
+
Matrix matrix = new Matrix();
|
|
87
|
+
|
|
88
|
+
switch (exifOrientation) {
|
|
89
|
+
case ExifInterface.ORIENTATION_ROTATE_90:
|
|
90
|
+
matrix.postRotate(90);
|
|
91
|
+
break;
|
|
92
|
+
case ExifInterface.ORIENTATION_ROTATE_180:
|
|
93
|
+
matrix.postRotate(180);
|
|
94
|
+
break;
|
|
95
|
+
case ExifInterface.ORIENTATION_ROTATE_270:
|
|
96
|
+
matrix.postRotate(270);
|
|
97
|
+
break;
|
|
98
|
+
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
|
|
99
|
+
matrix.setScale(-1, 1);
|
|
100
|
+
break;
|
|
101
|
+
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
|
|
102
|
+
matrix.setScale(1, -1);
|
|
103
|
+
break;
|
|
104
|
+
case ExifInterface.ORIENTATION_TRANSPOSE:
|
|
105
|
+
matrix.postRotate(90);
|
|
106
|
+
matrix.postScale(-1, 1);
|
|
107
|
+
break;
|
|
108
|
+
case ExifInterface.ORIENTATION_TRANSVERSE:
|
|
109
|
+
matrix.postRotate(270);
|
|
110
|
+
matrix.postScale(-1, 1);
|
|
111
|
+
break;
|
|
112
|
+
case ExifInterface.ORIENTATION_NORMAL:
|
|
113
|
+
case ExifInterface.ORIENTATION_UNDEFINED:
|
|
114
|
+
default:
|
|
115
|
+
return bitmap;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
Bitmap rotatedBitmap = Bitmap.createBitmap(
|
|
120
|
+
bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true
|
|
121
|
+
);
|
|
122
|
+
return rotatedBitmap;
|
|
123
|
+
} catch (OutOfMemoryError e) {
|
|
124
|
+
Log.e(TAG, "Out of memory rotating bitmap", e);
|
|
125
|
+
return bitmap;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generate a thumbnail from an already orientation-corrected image file
|
|
131
|
+
* @param imageFile Source image file (must already have corrected orientation)
|
|
132
|
+
* @param thumbnailSize Target thumbnail size (will be square)
|
|
133
|
+
* @return Base64 data URI of thumbnail, or null if failed
|
|
134
|
+
*/
|
|
135
|
+
public static String generateThumbnail(File imageFile, int thumbnailSize) {
|
|
136
|
+
if (imageFile == null || !imageFile.exists()) {
|
|
137
|
+
Log.e(TAG, "Image file does not exist for thumbnail generation");
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
143
|
+
options.inSampleSize = calculateSampleSize(imageFile, thumbnailSize * 2);
|
|
144
|
+
|
|
145
|
+
Bitmap originalBitmap = BitmapFactory.decodeFile(imageFile.getAbsolutePath(), options);
|
|
146
|
+
if (originalBitmap == null) {
|
|
147
|
+
Log.e(TAG, "Failed to decode image file for thumbnail");
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Bitmap thumbnail = createSquareThumbnail(originalBitmap, thumbnailSize);
|
|
152
|
+
originalBitmap.recycle();
|
|
153
|
+
|
|
154
|
+
if (thumbnail == null) {
|
|
155
|
+
Log.e(TAG, "Failed to generate thumbnail");
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
String base64Thumbnail = ThumbnailGenerator.bitmapToBase64(thumbnail, 85);
|
|
160
|
+
thumbnail.recycle();
|
|
161
|
+
|
|
162
|
+
return base64Thumbnail;
|
|
163
|
+
|
|
164
|
+
} catch (Exception e) {
|
|
165
|
+
Log.e(TAG, "Error generating thumbnail: " + e.getMessage(), e);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Create a square thumbnail by center-cropping
|
|
172
|
+
* @param source Source bitmap
|
|
173
|
+
* @param size Target size
|
|
174
|
+
* @return Square thumbnail bitmap
|
|
175
|
+
*/
|
|
176
|
+
private static Bitmap createSquareThumbnail(Bitmap source, int size) {
|
|
177
|
+
int sourceWidth = source.getWidth();
|
|
178
|
+
int sourceHeight = source.getHeight();
|
|
179
|
+
|
|
180
|
+
int cropSize = Math.min(sourceWidth, sourceHeight);
|
|
181
|
+
int x = (sourceWidth - cropSize) / 2;
|
|
182
|
+
int y = (sourceHeight - cropSize) / 2;
|
|
183
|
+
|
|
184
|
+
Bitmap croppedBitmap = Bitmap.createBitmap(source, x, y, cropSize, cropSize);
|
|
185
|
+
Bitmap scaledBitmap = Bitmap.createScaledBitmap(croppedBitmap, size, size, true);
|
|
186
|
+
|
|
187
|
+
if (croppedBitmap != scaledBitmap) {
|
|
188
|
+
croppedBitmap.recycle();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return scaledBitmap;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Calculate sample size for efficient loading
|
|
196
|
+
* @param imageFile Image file
|
|
197
|
+
* @param targetSize Target size
|
|
198
|
+
* @return Sample size for BitmapFactory
|
|
199
|
+
*/
|
|
200
|
+
private static int calculateSampleSize(File imageFile, int targetSize) {
|
|
201
|
+
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
202
|
+
options.inJustDecodeBounds = true;
|
|
203
|
+
BitmapFactory.decodeFile(imageFile.getAbsolutePath(), options);
|
|
204
|
+
|
|
205
|
+
int sampleSize = 1;
|
|
206
|
+
int maxDimension = Math.max(options.outWidth, options.outHeight);
|
|
207
|
+
|
|
208
|
+
while (maxDimension / sampleSize > targetSize) {
|
|
209
|
+
sampleSize *= 2;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return sampleSize;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
package dev.hemang.cameramulticapture;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap;
|
|
4
|
+
import android.util.Base64;
|
|
5
|
+
import android.util.Log;
|
|
6
|
+
|
|
7
|
+
import java.io.ByteArrayOutputStream;
|
|
8
|
+
import java.io.File;
|
|
9
|
+
|
|
10
|
+
public class ThumbnailGenerator {
|
|
11
|
+
|
|
12
|
+
private static final String TAG = "ThumbnailGenerator";
|
|
13
|
+
private static final int DEFAULT_THUMBNAIL_SIZE = 200;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate a thumbnail from an already orientation-corrected image file
|
|
17
|
+
* @param imageFile Image file (must already have corrected orientation)
|
|
18
|
+
* @param thumbnailSize Target thumbnail size
|
|
19
|
+
* @return Base64 data URI of thumbnail
|
|
20
|
+
*/
|
|
21
|
+
public static String generateThumbnail(File imageFile, int thumbnailSize) {
|
|
22
|
+
return ImageUtils.generateThumbnail(imageFile, thumbnailSize);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generate a thumbnail with default size
|
|
27
|
+
* @param imageFile Image file
|
|
28
|
+
* @return Base64 data URI of thumbnail
|
|
29
|
+
*/
|
|
30
|
+
public static String generateThumbnail(File imageFile) {
|
|
31
|
+
return generateThumbnail(imageFile, DEFAULT_THUMBNAIL_SIZE);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Convert bitmap to Base64 data URI
|
|
36
|
+
* @param bitmap Source bitmap
|
|
37
|
+
* @param quality JPEG quality (0-100)
|
|
38
|
+
* @return Base64 data URI string
|
|
39
|
+
*/
|
|
40
|
+
public static String bitmapToBase64(Bitmap bitmap, int quality) {
|
|
41
|
+
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
|
42
|
+
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream);
|
|
43
|
+
byte[] bytes = outputStream.toByteArray();
|
|
44
|
+
|
|
45
|
+
String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP);
|
|
46
|
+
return "data:image/jpeg;base64," + base64;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { TorchState } from '../definitions';
|
|
2
|
+
import type { CameraImageData, CameraMultiCapturePlugin, CameraVideoData } from '../definitions';
|
|
3
|
+
import type { CameraOverlayUIOptions } from '../types/ui-types';
|
|
4
|
+
/**
|
|
5
|
+
* Interface for camera rectangle dimensions
|
|
6
|
+
*/
|
|
7
|
+
export interface CameraRect {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Manages camera operations
|
|
15
|
+
*/
|
|
16
|
+
export declare class CameraController {
|
|
17
|
+
private plugin;
|
|
18
|
+
private options;
|
|
19
|
+
private flashMode;
|
|
20
|
+
private flashAutoModeEnabled;
|
|
21
|
+
private torchState;
|
|
22
|
+
private isRecording;
|
|
23
|
+
private currentZoom;
|
|
24
|
+
private preRecordingState;
|
|
25
|
+
private availableCameras;
|
|
26
|
+
constructor(plugin: CameraMultiCapturePlugin, options: CameraOverlayUIOptions);
|
|
27
|
+
/**
|
|
28
|
+
* Initializes the camera
|
|
29
|
+
*/
|
|
30
|
+
initialize(containerElement: HTMLElement, quality: number): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Captures an image
|
|
33
|
+
*/
|
|
34
|
+
captureImage(): Promise<CameraImageData | undefined>;
|
|
35
|
+
startVideoRecording(): Promise<void>;
|
|
36
|
+
stopVideoRecording(): Promise<CameraVideoData | undefined>;
|
|
37
|
+
private restorePreRecordingState;
|
|
38
|
+
/**
|
|
39
|
+
* Returns whether video recording is currently active.
|
|
40
|
+
*/
|
|
41
|
+
getRecordingState(): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Sets the zoom level
|
|
44
|
+
*/
|
|
45
|
+
setZoom(level: number): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* Gets the current zoom level (tracked after setZoom calls)
|
|
48
|
+
*/
|
|
49
|
+
getCurrentZoom(): number;
|
|
50
|
+
/**
|
|
51
|
+
* Gets available cameras information
|
|
52
|
+
*/
|
|
53
|
+
getAvailableCameras(): Promise<{
|
|
54
|
+
hasUltrawide: boolean;
|
|
55
|
+
hasWide: boolean;
|
|
56
|
+
hasTelephoto: boolean;
|
|
57
|
+
ultrawideZoomFactor?: number;
|
|
58
|
+
wideZoomFactor: number;
|
|
59
|
+
telephotoZoomFactor?: number;
|
|
60
|
+
}>;
|
|
61
|
+
/**
|
|
62
|
+
* Switches to a physical camera based on zoom factor
|
|
63
|
+
* This will switch to the appropriate lens (ultrawide, wide, telephoto)
|
|
64
|
+
*/
|
|
65
|
+
switchToPhysicalCamera(zoomFactor: number): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Gets available zoom levels for the current camera
|
|
68
|
+
*/
|
|
69
|
+
getAvailableZoomLevels(): Promise<{
|
|
70
|
+
minZoom: number;
|
|
71
|
+
maxZoom: number;
|
|
72
|
+
presetLevels: number[];
|
|
73
|
+
}>;
|
|
74
|
+
/**
|
|
75
|
+
* Switches between front and back camera
|
|
76
|
+
*/
|
|
77
|
+
switchCamera(): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Stops the camera
|
|
80
|
+
*/
|
|
81
|
+
stop(): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Sets the flash mode
|
|
84
|
+
*/
|
|
85
|
+
setFlash(mode: 'on' | 'off' | 'auto'): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Gets the current flash mode
|
|
88
|
+
*/
|
|
89
|
+
getFlashMode(): 'on' | 'off' | 'auto';
|
|
90
|
+
/**
|
|
91
|
+
* Toggles flash mode between off, on, and optionally auto
|
|
92
|
+
*/
|
|
93
|
+
toggleFlash(): Promise<'on' | 'off' | 'auto'>;
|
|
94
|
+
/**
|
|
95
|
+
* Gets whether auto mode is enabled
|
|
96
|
+
*/
|
|
97
|
+
isFlashAutoModeEnabled(): boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Sets the torch (flashlight) state.
|
|
100
|
+
*/
|
|
101
|
+
setTorch(state: TorchState): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* Gets the current torch state.
|
|
104
|
+
*/
|
|
105
|
+
getTorch(): Promise<TorchState>;
|
|
106
|
+
/**
|
|
107
|
+
* Toggles the torch state and returns the new value.
|
|
108
|
+
*/
|
|
109
|
+
toggleTorch(): Promise<TorchState>;
|
|
110
|
+
/**
|
|
111
|
+
* Updates the camera preview rectangle dimensions
|
|
112
|
+
* Call this method when the container size changes (e.g., orientation change)
|
|
113
|
+
*/
|
|
114
|
+
refresh(): Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Gets smart zoom levels that include physical camera switches
|
|
117
|
+
* Returns an array of zoom levels and whether each is a physical camera
|
|
118
|
+
*/
|
|
119
|
+
getSmartZoomLevels(): Promise<{
|
|
120
|
+
level: number;
|
|
121
|
+
isPhysicalCamera: boolean;
|
|
122
|
+
}[]>;
|
|
123
|
+
/**
|
|
124
|
+
* Performs smart zoom - switches physical cameras when appropriate
|
|
125
|
+
*/
|
|
126
|
+
performSmartZoom(targetZoom: number): Promise<void>;
|
|
127
|
+
}
|