This commit is contained in:
metacryst
2026-01-09 11:14:27 -06:00
parent cf03c95664
commit 637c9e4674
2149 changed files with 527743 additions and 0 deletions

83
node_modules/@capacitor/camera/android/build.gradle generated vendored Normal file
View File

@@ -0,0 +1,83 @@
ext {
capacitorVersion = System.getenv('CAPACITOR_VERSION')
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
androidxExifInterfaceVersion = project.hasProperty('androidxExifInterfaceVersion') ? rootProject.ext.androidxExifInterfaceVersion : '1.3.7'
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
androidxMaterialVersion = project.hasProperty('androidxMaterialVersion') ? rootProject.ext.androidxMaterialVersion : '1.12.0'
}
buildscript {
repositories {
google()
mavenCentral()
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath 'com.android.tools.build:gradle:8.7.2'
if (System.getenv("CAP_PLUGIN_PUBLISH") == "true") {
classpath 'io.github.gradle-nexus:publish-plugin:1.3.0'
}
}
}
apply plugin: 'com.android.library'
if (System.getenv("CAP_PLUGIN_PUBLISH") == "true") {
apply plugin: 'io.github.gradle-nexus.publish-plugin'
apply from: file('../../scripts/android/publish-root.gradle')
apply from: file('../../scripts/android/publish-module.gradle')
}
android {
namespace "com.capacitorjs.plugins.camera"
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
defaultConfig {
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
publishing {
singleVariant("release")
}
}
repositories {
google()
mavenCentral()
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
if (System.getenv("CAP_PLUGIN_PUBLISH") == "true") {
implementation "com.capacitorjs:core:$capacitorVersion"
} else {
implementation project(':capacitor-android')
}
implementation "androidx.exifinterface:exifinterface:$androidxExifInterfaceVersion"
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation "com.google.android.material:material:$androidxMaterialVersion"
testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE" />
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,124 @@
package com.capacitorjs.plugins.camera;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.DialogInterface;
import android.graphics.Color;
import android.view.View;
import android.view.Window;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import java.util.List;
public class CameraBottomSheetDialogFragment extends BottomSheetDialogFragment {
interface BottomSheetOnSelectedListener {
void onSelected(int index);
}
interface BottomSheetOnCanceledListener {
void onCanceled();
}
private BottomSheetOnSelectedListener selectedListener;
private BottomSheetOnCanceledListener canceledListener;
private List<String> options;
private String title;
void setTitle(String title) {
this.title = title;
}
void setOptions(List<String> options, BottomSheetOnSelectedListener selectedListener, BottomSheetOnCanceledListener canceledListener) {
this.options = options;
this.selectedListener = selectedListener;
this.canceledListener = canceledListener;
}
@Override
public void onCancel(DialogInterface dialog) {
super.onCancel(dialog);
if (canceledListener != null) {
this.canceledListener.onCanceled();
}
}
private BottomSheetBehavior.BottomSheetCallback mBottomSheetBehaviorCallback = new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
dismiss();
}
}
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {}
};
@Override
@SuppressLint("RestrictedApi")
public void setupDialog(Dialog dialog, int style) {
super.setupDialog(dialog, style);
if (options == null || options.size() == 0) {
return;
}
Window w = dialog.getWindow();
final float scale = getResources().getDisplayMetrics().density;
float layoutPaddingDp16 = 16.0f;
float layoutPaddingDp12 = 12.0f;
float layoutPaddingDp8 = 8.0f;
int layoutPaddingPx16 = (int) (layoutPaddingDp16 * scale + 0.5f);
int layoutPaddingPx12 = (int) (layoutPaddingDp12 * scale + 0.5f);
int layoutPaddingPx8 = (int) (layoutPaddingDp8 * scale + 0.5f);
CoordinatorLayout parentLayout = new CoordinatorLayout(getContext());
LinearLayout layout = new LinearLayout(getContext());
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(layoutPaddingPx16, layoutPaddingPx16, layoutPaddingPx16, layoutPaddingPx16);
TextView ttv = new TextView(getContext());
ttv.setTextColor(Color.parseColor("#757575"));
ttv.setPadding(layoutPaddingPx8, layoutPaddingPx8, layoutPaddingPx8, layoutPaddingPx8);
ttv.setText(title);
layout.addView(ttv);
for (int i = 0; i < options.size(); i++) {
final int optionIndex = i;
TextView tv = new TextView(getContext());
tv.setTextColor(Color.parseColor("#000000"));
tv.setPadding(layoutPaddingPx12, layoutPaddingPx12, layoutPaddingPx12, layoutPaddingPx12);
tv.setText(options.get(i));
tv.setOnClickListener(
view -> {
if (selectedListener != null) {
selectedListener.onSelected(optionIndex);
}
dismiss();
}
);
layout.addView(tv);
}
parentLayout.addView(layout.getRootView());
dialog.setContentView(parentLayout.getRootView());
CoordinatorLayout.LayoutParams params = (CoordinatorLayout.LayoutParams) ((View) parentLayout.getParent()).getLayoutParams();
CoordinatorLayout.Behavior<View> behavior = params.getBehavior();
if (behavior instanceof BottomSheetBehavior) {
BottomSheetBehavior<View> bottomSheetBehavior = (BottomSheetBehavior<View>) behavior;
bottomSheetBehavior.addBottomSheetCallback(mBottomSheetBehaviorCallback);
bottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
}

View File

@@ -0,0 +1,923 @@
package com.capacitorjs.plugins.camera;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.util.Base64;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.ActivityResultRegistryOwner;
import androidx.activity.result.PickVisualMediaRequest;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.core.content.FileProvider;
import com.getcapacitor.FileUtils;
import com.getcapacitor.JSArray;
import com.getcapacitor.JSObject;
import com.getcapacitor.Logger;
import com.getcapacitor.PermissionState;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;
import com.getcapacitor.annotation.ActivityCallback;
import com.getcapacitor.annotation.CapacitorPlugin;
import com.getcapacitor.annotation.Permission;
import com.getcapacitor.annotation.PermissionCallback;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import org.json.JSONException;
/**
* The Camera plugin makes it easy to take a photo or have the user select a photo
* from their albums.
*
* On Android, this plugin sends an intent that opens the stock Camera app.
*
* Adapted from https://developer.android.com/training/camera/photobasics.html
*/
@SuppressLint("InlinedApi")
@CapacitorPlugin(
name = "Camera",
permissions = {
@Permission(strings = { Manifest.permission.CAMERA }, alias = CameraPlugin.CAMERA),
@Permission(strings = {}, alias = CameraPlugin.PHOTOS),
// SDK VERSIONS 29 AND BELOW
@Permission(
strings = { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE },
alias = CameraPlugin.SAVE_GALLERY
),
/*
SDK VERSIONS 30-32
This alias is a placeholder and the SAVE_GALLERY alias will be updated to use this permission
so that the end user does not need to explicitly use separate aliases depending
on the SDK version.
*/
@Permission(strings = { Manifest.permission.READ_EXTERNAL_STORAGE }, alias = CameraPlugin.READ_EXTERNAL_STORAGE)
}
)
public class CameraPlugin extends Plugin {
// Permission alias constants
static final String CAMERA = "camera";
static final String PHOTOS = "photos";
static final String SAVE_GALLERY = "saveGallery";
static final String READ_EXTERNAL_STORAGE = "readExternalStorage";
// Message constants
private static final String INVALID_RESULT_TYPE_ERROR = "Invalid resultType option";
private static final String PERMISSION_DENIED_ERROR_CAMERA = "User denied access to camera";
private static final String NO_CAMERA_ERROR = "Device doesn't have a camera available";
private static final String NO_CAMERA_ACTIVITY_ERROR = "Unable to resolve camera activity";
private static final String NO_PHOTO_ACTIVITY_ERROR = "Unable to resolve photo activity";
private static final String IMAGE_FILE_SAVE_ERROR = "Unable to create photo on disk";
private static final String IMAGE_PROCESS_NO_FILE_ERROR = "Unable to process image, file not found on disk";
private static final String UNABLE_TO_PROCESS_IMAGE = "Unable to process image";
private static final String IMAGE_EDIT_ERROR = "Unable to edit image";
private static final String IMAGE_GALLERY_SAVE_ERROR = "Unable to save the image in the gallery";
private static final String USER_CANCELLED = "User cancelled photos app";
private String imageFileSavePath;
private String imageEditedFileSavePath;
private Uri imageFileUri;
private Uri imagePickedContentUri;
private boolean isEdited = false;
private boolean isFirstRequest = true;
private boolean isSaved = false;
private ActivityResultLauncher<PickVisualMediaRequest> pickMultipleMedia = null;
private ActivityResultLauncher<PickVisualMediaRequest> pickMedia = null;
private final AtomicInteger mNextLocalRequestCode = new AtomicInteger();
private CameraSettings settings = new CameraSettings();
@Override
public void load() {
super.load();
}
@PluginMethod
public void getPhoto(PluginCall call) {
isEdited = false;
settings = getSettings(call);
doShow(call);
}
@PluginMethod
public void pickImages(PluginCall call) {
settings = getSettings(call);
openPhotos(call, true);
}
@PluginMethod
public void pickLimitedLibraryPhotos(PluginCall call) {
call.unimplemented("not supported on android");
}
@PluginMethod
public void getLimitedLibraryPhotos(PluginCall call) {
call.unimplemented("not supported on android");
}
private void doShow(PluginCall call) {
switch (settings.getSource()) {
case CAMERA:
showCamera(call);
break;
case PHOTOS:
showPhotos(call);
break;
default:
showPrompt(call);
break;
}
}
private void showPrompt(final PluginCall call) {
// We have all necessary permissions, open the camera
List<String> options = new ArrayList<>();
options.add(call.getString("promptLabelPhoto", "From Photos"));
options.add(call.getString("promptLabelPicture", "Take Picture"));
final CameraBottomSheetDialogFragment fragment = new CameraBottomSheetDialogFragment();
fragment.setTitle(call.getString("promptLabelHeader", "Photo"));
fragment.setOptions(
options,
index -> {
if (index == 0) {
settings.setSource(CameraSource.PHOTOS);
openPhotos(call);
} else if (index == 1) {
settings.setSource(CameraSource.CAMERA);
openCamera(call);
}
},
() -> call.reject(USER_CANCELLED)
);
fragment.show(getActivity().getSupportFragmentManager(), "capacitorModalsActionSheet");
}
private void showCamera(final PluginCall call) {
if (!getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
call.reject(NO_CAMERA_ERROR);
return;
}
openCamera(call);
}
private void showPhotos(final PluginCall call) {
openPhotos(call);
}
private boolean checkCameraPermissions(PluginCall call) {
// if the manifest does not contain the camera permissions key, we don't need to ask the user
boolean needCameraPerms = isPermissionDeclared(CAMERA);
boolean hasCameraPerms = !needCameraPerms || getPermissionState(CAMERA) == PermissionState.GRANTED;
boolean hasGalleryPerms = getPermissionState(SAVE_GALLERY) == PermissionState.GRANTED;
// If we want to save to the gallery, we need two permissions
// actually we only need permissions to save to gallery for Android <= 9 (API 28)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// we might still need to request permission for the camera
if (!hasCameraPerms) {
requestPermissionForAlias(CAMERA, call, "cameraPermissionsCallback");
return false;
}
return true;
}
// we need to request permissions to save to gallery for Android <= 9
if (settings.isSaveToGallery() && !(hasCameraPerms && hasGalleryPerms) && isFirstRequest) {
isFirstRequest = false;
String[] aliases;
if (needCameraPerms) {
aliases = new String[] { CAMERA, SAVE_GALLERY };
} else {
aliases = new String[] { SAVE_GALLERY };
}
requestPermissionForAliases(aliases, call, "cameraPermissionsCallback");
return false;
}
// If we don't need to save to the gallery, we can just ask for camera permissions
else if (!hasCameraPerms) {
requestPermissionForAlias(CAMERA, call, "cameraPermissionsCallback");
return false;
}
return true;
}
/**
* Completes the plugin call after a camera permission request
*
* @see #getPhoto(PluginCall)
* @param call the plugin call
*/
@PermissionCallback
private void cameraPermissionsCallback(PluginCall call) {
if (call.getMethodName().equals("pickImages")) {
openPhotos(call, true);
} else {
if (settings.getSource() == CameraSource.CAMERA && getPermissionState(CAMERA) != PermissionState.GRANTED) {
Logger.debug(getLogTag(), "User denied camera permission: " + getPermissionState(CAMERA).toString());
call.reject(PERMISSION_DENIED_ERROR_CAMERA);
return;
}
doShow(call);
}
}
@Override
protected void requestPermissionForAliases(@NonNull String[] aliases, @NonNull PluginCall call, @NonNull String callbackName) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
for (int i = 0; i < aliases.length; i++) {
if (aliases[i].equals(SAVE_GALLERY)) {
aliases[i] = READ_EXTERNAL_STORAGE;
}
}
}
super.requestPermissionForAliases(aliases, call, callbackName);
}
private CameraSettings getSettings(PluginCall call) {
CameraSettings settings = new CameraSettings();
settings.setResultType(getResultType(call.getString("resultType")));
settings.setSaveToGallery(call.getBoolean("saveToGallery", CameraSettings.DEFAULT_SAVE_IMAGE_TO_GALLERY));
settings.setAllowEditing(call.getBoolean("allowEditing", false));
settings.setQuality(call.getInt("quality", CameraSettings.DEFAULT_QUALITY));
settings.setWidth(call.getInt("width", 0));
settings.setHeight(call.getInt("height", 0));
settings.setShouldResize(settings.getWidth() > 0 || settings.getHeight() > 0);
settings.setShouldCorrectOrientation(call.getBoolean("correctOrientation", CameraSettings.DEFAULT_CORRECT_ORIENTATION));
try {
settings.setSource(CameraSource.valueOf(call.getString("source", CameraSource.PROMPT.getSource())));
} catch (IllegalArgumentException ex) {
settings.setSource(CameraSource.PROMPT);
}
return settings;
}
private CameraResultType getResultType(String resultType) {
if (resultType == null) {
return null;
}
try {
return CameraResultType.valueOf(resultType.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException ex) {
Logger.debug(getLogTag(), "Invalid result type \"" + resultType + "\", defaulting to base64");
return CameraResultType.BASE64;
}
}
public void openCamera(final PluginCall call) {
if (checkCameraPermissions(call)) {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(getContext().getPackageManager()) != null) {
// If we will be saving the photo, send the target file along
try {
String appId = getAppId();
File photoFile = CameraUtils.createImageFile(getActivity());
imageFileSavePath = photoFile.getAbsolutePath();
// TODO: Verify provider config exists
imageFileUri = FileProvider.getUriForFile(getActivity(), appId + ".fileprovider", photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri);
} catch (Exception ex) {
call.reject(IMAGE_FILE_SAVE_ERROR, ex);
return;
}
startActivityForResult(call, takePictureIntent, "processCameraImage");
} else {
call.reject(NO_CAMERA_ACTIVITY_ERROR);
}
}
}
public void openPhotos(final PluginCall call) {
openPhotos(call, false);
}
private <I, O> ActivityResultLauncher<I> registerActivityResultLauncher(
ActivityResultContract<I, O> contract,
ActivityResultCallback<O> callback
) {
String key = "cap_activity_rq#" + mNextLocalRequestCode.getAndIncrement();
if (bridge.getFragment() != null) {
Object host = bridge.getFragment().getHost();
if (host instanceof ActivityResultRegistryOwner) {
return ((ActivityResultRegistryOwner) host).getActivityResultRegistry().register(key, contract, callback);
}
return bridge.getFragment().requireActivity().getActivityResultRegistry().register(key, contract, callback);
}
return bridge.getActivity().getActivityResultRegistry().register(key, contract, callback);
}
private ActivityResultContract<PickVisualMediaRequest, List<Uri>> getContractForCall(final PluginCall call) {
int limit = call.getInt("limit", 0);
if (limit > 1) {
return new ActivityResultContracts.PickMultipleVisualMedia(limit);
} else {
return new ActivityResultContracts.PickMultipleVisualMedia();
}
}
private void openPhotos(final PluginCall call, boolean multiple) {
try {
if (multiple) {
pickMultipleMedia =
registerActivityResultLauncher(
getContractForCall(call),
uris -> {
if (!uris.isEmpty()) {
Executor executor = Executors.newSingleThreadExecutor();
executor.execute(
() -> {
JSObject ret = new JSObject();
JSArray photos = new JSArray();
for (Uri imageUri : uris) {
try {
JSObject processResult = processPickedImages(imageUri);
if (
processResult.getString("error") != null && !processResult.getString("error").isEmpty()
) {
call.reject(processResult.getString("error"));
return;
} else {
photos.put(processResult);
}
} catch (SecurityException ex) {
call.reject("SecurityException");
}
}
ret.put("photos", photos);
call.resolve(ret);
}
);
} else {
call.reject(USER_CANCELLED);
}
pickMultipleMedia.unregister();
}
);
pickMultipleMedia.launch(
new PickVisualMediaRequest.Builder().setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE).build()
);
} else {
pickMedia =
registerActivityResultLauncher(
new ActivityResultContracts.PickVisualMedia(),
uri -> {
if (uri != null) {
imagePickedContentUri = uri;
processPickedImage(uri, call);
} else {
call.reject(USER_CANCELLED);
}
pickMedia.unregister();
}
);
pickMedia.launch(
new PickVisualMediaRequest.Builder().setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE).build()
);
}
} catch (ActivityNotFoundException ex) {
call.reject(NO_PHOTO_ACTIVITY_ERROR);
}
}
@ActivityCallback
public void processCameraImage(PluginCall call, ActivityResult result) {
settings = getSettings(call);
if (imageFileSavePath == null) {
call.reject(IMAGE_PROCESS_NO_FILE_ERROR);
return;
}
// Load the image as a Bitmap
File f = new File(imageFileSavePath);
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
Uri contentUri = Uri.fromFile(f);
Bitmap bitmap = BitmapFactory.decodeFile(imageFileSavePath, bmOptions);
if (bitmap == null) {
call.reject(USER_CANCELLED);
return;
}
returnResult(call, bitmap, contentUri);
}
public void processPickedImage(PluginCall call, ActivityResult result) {
settings = getSettings(call);
Intent data = result.getData();
if (data == null) {
call.reject(USER_CANCELLED);
return;
}
Uri u = data.getData();
imagePickedContentUri = u;
processPickedImage(u, call);
}
@SuppressWarnings("deprecation")
private ArrayList<Parcelable> getLegacyParcelableArrayList(Bundle bundle, String key) {
return bundle.getParcelableArrayList(key);
}
private void processPickedImage(Uri imageUri, PluginCall call) {
InputStream imageStream = null;
try {
imageStream = getContext().getContentResolver().openInputStream(imageUri);
Bitmap bitmap = BitmapFactory.decodeStream(imageStream);
if (bitmap == null) {
call.reject("Unable to process bitmap");
return;
}
returnResult(call, bitmap, imageUri);
} catch (OutOfMemoryError err) {
call.reject("Out of memory");
} catch (FileNotFoundException ex) {
call.reject("No such image found", ex);
} finally {
if (imageStream != null) {
try {
imageStream.close();
} catch (IOException e) {
Logger.error(getLogTag(), UNABLE_TO_PROCESS_IMAGE, e);
}
}
}
}
private JSObject processPickedImages(Uri imageUri) {
InputStream imageStream = null;
JSObject ret = new JSObject();
try {
imageStream = getContext().getContentResolver().openInputStream(imageUri);
Bitmap bitmap = BitmapFactory.decodeStream(imageStream);
if (bitmap == null) {
ret.put("error", "Unable to process bitmap");
return ret;
}
ExifWrapper exif = ImageUtils.getExifData(getContext(), bitmap, imageUri);
try {
bitmap = prepareBitmap(bitmap, imageUri, exif);
} catch (IOException e) {
ret.put("error", UNABLE_TO_PROCESS_IMAGE);
return ret;
}
// Compress the final image and prepare for output to client
ByteArrayOutputStream bitmapOutputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, settings.getQuality(), bitmapOutputStream);
Uri newUri = getTempImage(imageUri, bitmapOutputStream);
exif.copyExif(newUri.getPath());
if (newUri != null) {
ret.put("format", "jpeg");
ret.put("exif", exif.toJson());
ret.put("path", newUri.toString());
ret.put("webPath", FileUtils.getPortablePath(getContext(), bridge.getLocalUrl(), newUri));
} else {
ret.put("error", UNABLE_TO_PROCESS_IMAGE);
}
return ret;
} catch (OutOfMemoryError err) {
ret.put("error", "Out of memory");
} catch (FileNotFoundException ex) {
ret.put("error", "No such image found");
Logger.error(getLogTag(), "No such image found", ex);
} finally {
if (imageStream != null) {
try {
imageStream.close();
} catch (IOException e) {
Logger.error(getLogTag(), UNABLE_TO_PROCESS_IMAGE, e);
}
}
}
return ret;
}
@ActivityCallback
private void processEditedImage(PluginCall call, ActivityResult result) {
isEdited = true;
settings = getSettings(call);
if (result.getResultCode() == Activity.RESULT_CANCELED) {
// User cancelled the edit operation, if this file was picked from photos,
// process the original picked image, otherwise process it as a camera photo
if (imagePickedContentUri != null) {
processPickedImage(imagePickedContentUri, call);
} else {
processCameraImage(call, result);
}
} else {
processPickedImage(call, result);
}
}
/**
* Save the modified image on the same path,
* or on a temporary location if it's a content url
* @param uri
* @param is
* @return
* @throws IOException
*/
private Uri saveImage(Uri uri, InputStream is) throws IOException {
File outFile = null;
if (uri.getScheme().equals("content")) {
outFile = getTempFile(uri);
} else {
outFile = new File(uri.getPath());
}
try {
writePhoto(outFile, is);
} catch (FileNotFoundException ex) {
// Some gallery apps return read only file url, create a temporary file for modifications
outFile = getTempFile(uri);
writePhoto(outFile, is);
}
return Uri.fromFile(outFile);
}
private void writePhoto(File outFile, InputStream is) throws IOException {
FileOutputStream fos = new FileOutputStream(outFile);
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.close();
}
private File getTempFile(Uri uri) {
String filename = Uri.parse(Uri.decode(uri.toString())).getLastPathSegment();
if (!filename.contains(".jpg") && !filename.contains(".jpeg")) {
filename += "." + (new java.util.Date()).getTime() + ".jpeg";
}
File cacheDir = getContext().getCacheDir();
return new File(cacheDir, filename);
}
/**
* After processing the image, return the final result back to the caller.
* @param call
* @param bitmap
* @param u
*/
@SuppressWarnings("deprecation")
private void returnResult(PluginCall call, Bitmap bitmap, Uri u) {
ExifWrapper exif = ImageUtils.getExifData(getContext(), bitmap, u);
try {
bitmap = prepareBitmap(bitmap, u, exif);
} catch (IOException e) {
call.reject(UNABLE_TO_PROCESS_IMAGE);
return;
}
// Compress the final image and prepare for output to client
ByteArrayOutputStream bitmapOutputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, settings.getQuality(), bitmapOutputStream);
if (settings.isAllowEditing() && !isEdited) {
editImage(call, u, bitmapOutputStream);
return;
}
boolean saveToGallery = call.getBoolean("saveToGallery", CameraSettings.DEFAULT_SAVE_IMAGE_TO_GALLERY);
if (saveToGallery && (imageEditedFileSavePath != null || imageFileSavePath != null)) {
isSaved = true;
try {
String fileToSavePath = imageEditedFileSavePath != null ? imageEditedFileSavePath : imageFileSavePath;
File fileToSave = new File(fileToSavePath);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ContentResolver resolver = getContext().getContentResolver();
ContentValues values = new ContentValues();
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileToSave.getName());
values.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM);
final Uri contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Uri uri = resolver.insert(contentUri, values);
if (uri == null) {
throw new IOException("Failed to create new MediaStore record.");
}
OutputStream stream = resolver.openOutputStream(uri);
if (stream == null) {
throw new IOException("Failed to open output stream.");
}
Boolean inserted = bitmap.compress(Bitmap.CompressFormat.JPEG, settings.getQuality(), stream);
if (!inserted) {
isSaved = false;
}
} else {
String inserted = MediaStore.Images.Media.insertImage(
getContext().getContentResolver(),
fileToSavePath,
fileToSave.getName(),
""
);
if (inserted == null) {
isSaved = false;
}
}
} catch (FileNotFoundException e) {
isSaved = false;
Logger.error(getLogTag(), IMAGE_GALLERY_SAVE_ERROR, e);
} catch (IOException e) {
isSaved = false;
Logger.error(getLogTag(), IMAGE_GALLERY_SAVE_ERROR, e);
}
}
if (settings.getResultType() == CameraResultType.BASE64) {
returnBase64(call, exif, bitmapOutputStream);
} else if (settings.getResultType() == CameraResultType.URI) {
returnFileURI(call, exif, bitmap, u, bitmapOutputStream);
} else if (settings.getResultType() == CameraResultType.DATAURL) {
returnDataUrl(call, exif, bitmapOutputStream);
} else {
call.reject(INVALID_RESULT_TYPE_ERROR);
}
// Result returned, clear stored paths and images
if (settings.getResultType() != CameraResultType.URI) {
deleteImageFile();
}
imageFileSavePath = null;
imageFileUri = null;
imagePickedContentUri = null;
imageEditedFileSavePath = null;
}
private void deleteImageFile() {
if (imageFileSavePath != null && !settings.isSaveToGallery()) {
File photoFile = new File(imageFileSavePath);
if (photoFile.exists()) {
photoFile.delete();
}
}
}
private void returnFileURI(PluginCall call, ExifWrapper exif, Bitmap bitmap, Uri u, ByteArrayOutputStream bitmapOutputStream) {
Uri newUri = getTempImage(u, bitmapOutputStream);
exif.copyExif(newUri.getPath());
if (newUri != null) {
JSObject ret = new JSObject();
ret.put("format", "jpeg");
ret.put("exif", exif.toJson());
ret.put("path", newUri.toString());
ret.put("webPath", FileUtils.getPortablePath(getContext(), bridge.getLocalUrl(), newUri));
ret.put("saved", isSaved);
call.resolve(ret);
} else {
call.reject(UNABLE_TO_PROCESS_IMAGE);
}
}
private Uri getTempImage(Uri u, ByteArrayOutputStream bitmapOutputStream) {
ByteArrayInputStream bis = null;
Uri newUri = null;
try {
bis = new ByteArrayInputStream(bitmapOutputStream.toByteArray());
newUri = saveImage(u, bis);
} catch (IOException ex) {} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
Logger.error(getLogTag(), UNABLE_TO_PROCESS_IMAGE, e);
}
}
}
return newUri;
}
/**
* Apply our standard processing of the bitmap, returning a new one and
* recycling the old one in the process
* @param bitmap
* @param imageUri
* @param exif
* @return
*/
private Bitmap prepareBitmap(Bitmap bitmap, Uri imageUri, ExifWrapper exif) throws IOException {
if (settings.isShouldCorrectOrientation()) {
final Bitmap newBitmap = ImageUtils.correctOrientation(getContext(), bitmap, imageUri, exif);
bitmap = replaceBitmap(bitmap, newBitmap);
}
if (settings.isShouldResize()) {
final Bitmap newBitmap = ImageUtils.resize(bitmap, settings.getWidth(), settings.getHeight());
bitmap = replaceBitmap(bitmap, newBitmap);
}
return bitmap;
}
private Bitmap replaceBitmap(Bitmap bitmap, final Bitmap newBitmap) {
if (bitmap != newBitmap) {
bitmap.recycle();
}
bitmap = newBitmap;
return bitmap;
}
private void returnDataUrl(PluginCall call, ExifWrapper exif, ByteArrayOutputStream bitmapOutputStream) {
byte[] byteArray = bitmapOutputStream.toByteArray();
String encoded = Base64.encodeToString(byteArray, Base64.NO_WRAP);
JSObject data = new JSObject();
data.put("format", "jpeg");
data.put("dataUrl", "data:image/jpeg;base64," + encoded);
data.put("exif", exif.toJson());
call.resolve(data);
}
private void returnBase64(PluginCall call, ExifWrapper exif, ByteArrayOutputStream bitmapOutputStream) {
byte[] byteArray = bitmapOutputStream.toByteArray();
String encoded = Base64.encodeToString(byteArray, Base64.NO_WRAP);
JSObject data = new JSObject();
data.put("format", "jpeg");
data.put("base64String", encoded);
data.put("exif", exif.toJson());
call.resolve(data);
}
@Override
@PluginMethod
public void requestPermissions(PluginCall call) {
// If the camera permission is defined in the manifest, then we have to prompt the user
// or else we will get a security exception when trying to present the camera. If, however,
// it is not defined in the manifest then we don't need to prompt and it will just work.
if (isPermissionDeclared(CAMERA)) {
// just request normally
super.requestPermissions(call);
} else {
// the manifest does not define camera permissions, so we need to decide what to do
// first, extract the permissions being requested
JSArray providedPerms = call.getArray("permissions");
List<String> permsList = null;
if (providedPerms != null) {
try {
permsList = providedPerms.toList();
} catch (JSONException e) {}
}
if (
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU ||
(permsList != null && permsList.size() == 1 && (permsList.contains(CAMERA) || permsList.contains(PHOTOS)))
) {
// either we're on Android 13+ (storage permissions do not apply)
// or the only thing being asked for was the camera so we can just return the current state
checkPermissions(call);
} else {
requestPermissionForAlias(SAVE_GALLERY, call, "checkPermissions");
}
}
}
@Override
public Map<String, PermissionState> getPermissionStates() {
Map<String, PermissionState> permissionStates = super.getPermissionStates();
// If Camera is not in the manifest and therefore not required, say the permission is granted
if (!isPermissionDeclared(CAMERA)) {
permissionStates.put(CAMERA, PermissionState.GRANTED);
}
if (permissionStates.containsKey(PHOTOS)) {
permissionStates.put(PHOTOS, PermissionState.GRANTED);
}
// If the SDK version is 30 or higher, update the SAVE_GALLERY state to match the READ_EXTERNAL_STORAGE state.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
String alias = READ_EXTERNAL_STORAGE;
if (permissionStates.containsKey(alias)) {
permissionStates.put(SAVE_GALLERY, permissionStates.get(alias));
}
}
return permissionStates;
}
private void editImage(PluginCall call, Uri uri, ByteArrayOutputStream bitmapOutputStream) {
try {
Uri tempImage = getTempImage(uri, bitmapOutputStream);
Intent editIntent = createEditIntent(tempImage);
if (editIntent != null) {
startActivityForResult(call, editIntent, "processEditedImage");
} else {
call.reject(IMAGE_EDIT_ERROR);
}
} catch (Exception ex) {
call.reject(IMAGE_EDIT_ERROR, ex);
}
}
private Intent createEditIntent(Uri origPhotoUri) {
try {
File editFile = new File(origPhotoUri.getPath());
Uri editUri = FileProvider.getUriForFile(getActivity(), getContext().getPackageName() + ".fileprovider", editFile);
Intent editIntent = new Intent(Intent.ACTION_EDIT);
editIntent.setDataAndType(editUri, "image/*");
imageEditedFileSavePath = editFile.getAbsolutePath();
int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
editIntent.addFlags(flags);
editIntent.putExtra(MediaStore.EXTRA_OUTPUT, editUri);
List<ResolveInfo> resInfoList;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
resInfoList =
getContext()
.getPackageManager()
.queryIntentActivities(editIntent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY));
} else {
resInfoList = legacyQueryIntentActivities(editIntent);
}
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
getContext().grantUriPermission(packageName, editUri, flags);
}
return editIntent;
} catch (Exception ex) {
return null;
}
}
@SuppressWarnings("deprecation")
private List<ResolveInfo> legacyQueryIntentActivities(Intent intent) {
return getContext().getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
}
@Override
protected Bundle saveInstanceState() {
Bundle bundle = super.saveInstanceState();
if (bundle != null) {
bundle.putString("cameraImageFileSavePath", imageFileSavePath);
}
return bundle;
}
@Override
protected void restoreState(Bundle state) {
String storedImageFileSavePath = state.getString("cameraImageFileSavePath");
if (storedImageFileSavePath != null) {
imageFileSavePath = storedImageFileSavePath;
}
}
/**
* Unregister activity result launches to prevent leaks.
*/
@Override
protected void handleOnDestroy() {
if (pickMedia != null) {
pickMedia.unregister();
}
if (pickMultipleMedia != null) {
pickMultipleMedia.unregister();
}
}
}

View File

@@ -0,0 +1,17 @@
package com.capacitorjs.plugins.camera;
public enum CameraResultType {
BASE64("base64"),
URI("uri"),
DATAURL("dataUrl");
private String type;
CameraResultType(String type) {
this.type = type;
}
public String getType() {
return type;
}
}

View File

@@ -0,0 +1,90 @@
package com.capacitorjs.plugins.camera;
public class CameraSettings {
public static final int DEFAULT_QUALITY = 90;
public static final boolean DEFAULT_SAVE_IMAGE_TO_GALLERY = false;
public static final boolean DEFAULT_CORRECT_ORIENTATION = true;
private CameraResultType resultType = CameraResultType.BASE64;
private int quality = DEFAULT_QUALITY;
private boolean shouldResize = false;
private boolean shouldCorrectOrientation = DEFAULT_CORRECT_ORIENTATION;
private boolean saveToGallery = DEFAULT_SAVE_IMAGE_TO_GALLERY;
private boolean allowEditing = false;
private int width = 0;
private int height = 0;
private CameraSource source = CameraSource.PROMPT;
public CameraResultType getResultType() {
return resultType;
}
public void setResultType(CameraResultType resultType) {
this.resultType = resultType;
}
public int getQuality() {
return quality;
}
public void setQuality(int quality) {
this.quality = quality;
}
public boolean isShouldResize() {
return shouldResize;
}
public void setShouldResize(boolean shouldResize) {
this.shouldResize = shouldResize;
}
public boolean isShouldCorrectOrientation() {
return shouldCorrectOrientation;
}
public void setShouldCorrectOrientation(boolean shouldCorrectOrientation) {
this.shouldCorrectOrientation = shouldCorrectOrientation;
}
public boolean isSaveToGallery() {
return saveToGallery;
}
public void setSaveToGallery(boolean saveToGallery) {
this.saveToGallery = saveToGallery;
}
public boolean isAllowEditing() {
return allowEditing;
}
public void setAllowEditing(boolean allowEditing) {
this.allowEditing = allowEditing;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public CameraSource getSource() {
return source;
}
public void setSource(CameraSource source) {
this.source = source;
}
}

View File

@@ -0,0 +1,17 @@
package com.capacitorjs.plugins.camera;
public enum CameraSource {
PROMPT("PROMPT"),
CAMERA("CAMERA"),
PHOTOS("PHOTOS");
private String source;
CameraSource(String source) {
this.source = source;
}
public String getSource() {
return this.source;
}
}

View File

@@ -0,0 +1,34 @@
package com.capacitorjs.plugins.camera;
import android.app.Activity;
import android.net.Uri;
import android.os.Environment;
import androidx.core.content.FileProvider;
import com.getcapacitor.Logger;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class CameraUtils {
public static Uri createImageFileUri(Activity activity, String appId) throws IOException {
File photoFile = CameraUtils.createImageFile(activity);
return FileProvider.getUriForFile(activity, appId + ".fileprovider", photoFile);
}
public static File createImageFile(Activity activity) throws IOException {
// Create an image file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(imageFileName, /* prefix */".jpg", /* suffix */storageDir/* directory */);
return image;
}
protected static String getLogTag() {
return Logger.tags("CameraUtils");
}
}

View File

@@ -0,0 +1,205 @@
package com.capacitorjs.plugins.camera;
import static androidx.exifinterface.media.ExifInterface.*;
import androidx.exifinterface.media.ExifInterface;
import com.getcapacitor.JSObject;
public class ExifWrapper {
private final ExifInterface exif;
private final String[] attributes = new String[] {
TAG_APERTURE_VALUE,
TAG_ARTIST,
TAG_BITS_PER_SAMPLE,
TAG_BODY_SERIAL_NUMBER,
TAG_BRIGHTNESS_VALUE,
TAG_CAMERA_OWNER_NAME,
TAG_CFA_PATTERN,
TAG_COLOR_SPACE,
TAG_COMPONENTS_CONFIGURATION,
TAG_COMPRESSED_BITS_PER_PIXEL,
TAG_COMPRESSION,
TAG_CONTRAST,
TAG_COPYRIGHT,
TAG_CUSTOM_RENDERED,
TAG_DATETIME,
TAG_DATETIME_DIGITIZED,
TAG_DATETIME_ORIGINAL,
TAG_DEFAULT_CROP_SIZE,
TAG_DEVICE_SETTING_DESCRIPTION,
TAG_DIGITAL_ZOOM_RATIO,
TAG_DNG_VERSION,
TAG_EXIF_VERSION,
TAG_EXPOSURE_BIAS_VALUE,
TAG_EXPOSURE_INDEX,
TAG_EXPOSURE_MODE,
TAG_EXPOSURE_PROGRAM,
TAG_EXPOSURE_TIME,
TAG_FILE_SOURCE,
TAG_FLASH,
TAG_FLASHPIX_VERSION,
TAG_FLASH_ENERGY,
TAG_FOCAL_LENGTH,
TAG_FOCAL_LENGTH_IN_35MM_FILM,
TAG_FOCAL_PLANE_RESOLUTION_UNIT,
TAG_FOCAL_PLANE_X_RESOLUTION,
TAG_FOCAL_PLANE_Y_RESOLUTION,
TAG_F_NUMBER,
TAG_GAIN_CONTROL,
TAG_GAMMA,
TAG_GPS_ALTITUDE,
TAG_GPS_ALTITUDE_REF,
TAG_GPS_AREA_INFORMATION,
TAG_GPS_DATESTAMP,
TAG_GPS_DEST_BEARING,
TAG_GPS_DEST_BEARING_REF,
TAG_GPS_DEST_DISTANCE,
TAG_GPS_DEST_DISTANCE_REF,
TAG_GPS_DEST_LATITUDE,
TAG_GPS_DEST_LATITUDE_REF,
TAG_GPS_DEST_LONGITUDE,
TAG_GPS_DEST_LONGITUDE_REF,
TAG_GPS_DIFFERENTIAL,
TAG_GPS_DOP,
TAG_GPS_H_POSITIONING_ERROR,
TAG_GPS_IMG_DIRECTION,
TAG_GPS_IMG_DIRECTION_REF,
TAG_GPS_LATITUDE,
TAG_GPS_LATITUDE_REF,
TAG_GPS_LONGITUDE,
TAG_GPS_LONGITUDE_REF,
TAG_GPS_MAP_DATUM,
TAG_GPS_MEASURE_MODE,
TAG_GPS_PROCESSING_METHOD,
TAG_GPS_SATELLITES,
TAG_GPS_SPEED,
TAG_GPS_SPEED_REF,
TAG_GPS_STATUS,
TAG_GPS_TIMESTAMP,
TAG_GPS_TRACK,
TAG_GPS_TRACK_REF,
TAG_GPS_VERSION_ID,
TAG_IMAGE_DESCRIPTION,
TAG_IMAGE_LENGTH,
TAG_IMAGE_UNIQUE_ID,
TAG_IMAGE_WIDTH,
TAG_INTEROPERABILITY_INDEX,
TAG_ISO_SPEED,
TAG_ISO_SPEED_LATITUDE_YYY,
TAG_ISO_SPEED_LATITUDE_ZZZ,
TAG_JPEG_INTERCHANGE_FORMAT,
TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
TAG_LENS_MAKE,
TAG_LENS_MODEL,
TAG_LENS_SERIAL_NUMBER,
TAG_LENS_SPECIFICATION,
TAG_LIGHT_SOURCE,
TAG_MAKE,
TAG_MAKER_NOTE,
TAG_MAX_APERTURE_VALUE,
TAG_METERING_MODE,
TAG_MODEL,
TAG_NEW_SUBFILE_TYPE,
TAG_OECF,
TAG_OFFSET_TIME,
TAG_OFFSET_TIME_DIGITIZED,
TAG_OFFSET_TIME_ORIGINAL,
TAG_ORF_ASPECT_FRAME,
TAG_ORF_PREVIEW_IMAGE_LENGTH,
TAG_ORF_PREVIEW_IMAGE_START,
TAG_ORF_THUMBNAIL_IMAGE,
TAG_ORIENTATION,
TAG_PHOTOGRAPHIC_SENSITIVITY,
TAG_PHOTOMETRIC_INTERPRETATION,
TAG_PIXEL_X_DIMENSION,
TAG_PIXEL_Y_DIMENSION,
TAG_PLANAR_CONFIGURATION,
TAG_PRIMARY_CHROMATICITIES,
TAG_RECOMMENDED_EXPOSURE_INDEX,
TAG_REFERENCE_BLACK_WHITE,
TAG_RELATED_SOUND_FILE,
TAG_RESOLUTION_UNIT,
TAG_ROWS_PER_STRIP,
TAG_RW2_ISO,
TAG_RW2_JPG_FROM_RAW,
TAG_RW2_SENSOR_BOTTOM_BORDER,
TAG_RW2_SENSOR_LEFT_BORDER,
TAG_RW2_SENSOR_RIGHT_BORDER,
TAG_RW2_SENSOR_TOP_BORDER,
TAG_SAMPLES_PER_PIXEL,
TAG_SATURATION,
TAG_SCENE_CAPTURE_TYPE,
TAG_SCENE_TYPE,
TAG_SENSING_METHOD,
TAG_SENSITIVITY_TYPE,
TAG_SHARPNESS,
TAG_SHUTTER_SPEED_VALUE,
TAG_SOFTWARE,
TAG_SPATIAL_FREQUENCY_RESPONSE,
TAG_SPECTRAL_SENSITIVITY,
TAG_STANDARD_OUTPUT_SENSITIVITY,
TAG_STRIP_BYTE_COUNTS,
TAG_STRIP_OFFSETS,
TAG_SUBFILE_TYPE,
TAG_SUBJECT_AREA,
TAG_SUBJECT_DISTANCE,
TAG_SUBJECT_DISTANCE_RANGE,
TAG_SUBJECT_LOCATION,
TAG_SUBSEC_TIME,
TAG_SUBSEC_TIME_DIGITIZED,
TAG_SUBSEC_TIME_ORIGINAL,
TAG_THUMBNAIL_IMAGE_LENGTH,
TAG_THUMBNAIL_IMAGE_WIDTH,
TAG_TRANSFER_FUNCTION,
TAG_USER_COMMENT,
TAG_WHITE_BALANCE,
TAG_WHITE_POINT,
TAG_XMP,
TAG_X_RESOLUTION,
TAG_Y_CB_CR_COEFFICIENTS,
TAG_Y_CB_CR_POSITIONING,
TAG_Y_CB_CR_SUB_SAMPLING,
TAG_Y_RESOLUTION
};
public ExifWrapper(ExifInterface exif) {
this.exif = exif;
}
public JSObject toJson() {
JSObject ret = new JSObject();
if (this.exif == null) {
return ret;
}
for (int i = 0; i < attributes.length; i++) {
p(ret, attributes[i]);
}
return ret;
}
public void p(JSObject o, String tag) {
String val = exif.getAttribute(tag);
o.put(tag, val);
}
public void copyExif(String destFile) {
try {
ExifInterface destExif = new ExifInterface(destFile);
for (int i = 0; i < attributes.length; i++) {
String value = exif.getAttribute(attributes[i]);
if (value != null) {
destExif.setAttribute(attributes[i], value);
}
}
destExif.saveAttributes();
} catch (Exception ex) {}
}
public void resetOrientation() {
exif.resetOrientation();
}
}

View File

@@ -0,0 +1,124 @@
package com.capacitorjs.plugins.camera;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import androidx.exifinterface.media.ExifInterface;
import com.getcapacitor.Logger;
import java.io.IOException;
import java.io.InputStream;
public class ImageUtils {
/**
* Resize an image to the given max width and max height. Constraint can be put
* on one dimension, or both. Resize will always preserve aspect ratio.
* @param bitmap
* @param desiredMaxWidth
* @param desiredMaxHeight
* @return a new, scaled Bitmap
*/
public static Bitmap resize(Bitmap bitmap, final int desiredMaxWidth, final int desiredMaxHeight) {
return ImageUtils.resizePreservingAspectRatio(bitmap, desiredMaxWidth, desiredMaxHeight);
}
/**
* Resize an image to the given max width and max height. Constraint can be put
* on one dimension, or both. Resize will always preserve aspect ratio.
* @param bitmap
* @param desiredMaxWidth
* @param desiredMaxHeight
* @return a new, scaled Bitmap
*/
private static Bitmap resizePreservingAspectRatio(Bitmap bitmap, final int desiredMaxWidth, final int desiredMaxHeight) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
// 0 is treated as 'no restriction'
int maxHeight = desiredMaxHeight == 0 ? height : desiredMaxHeight;
int maxWidth = desiredMaxWidth == 0 ? width : desiredMaxWidth;
// resize with preserved aspect ratio
float newWidth = Math.min(width, maxWidth);
float newHeight = (height * newWidth) / width;
if (newHeight > maxHeight) {
newWidth = (width * maxHeight) / height;
newHeight = maxHeight;
}
return Bitmap.createScaledBitmap(bitmap, Math.round(newWidth), Math.round(newHeight), false);
}
/**
* Transform an image with the given matrix
* @param bitmap
* @param matrix
* @return
*/
private static Bitmap transform(final Bitmap bitmap, final Matrix matrix) {
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
/**
* Correct the orientation of an image by reading its exif information and rotating
* the appropriate amount for portrait mode
* @param bitmap
* @param imageUri
* @param exif
* @return
*/
public static Bitmap correctOrientation(final Context c, final Bitmap bitmap, final Uri imageUri, ExifWrapper exif) throws IOException {
final int orientation = getOrientation(c, imageUri);
if (orientation != 0) {
Matrix matrix = new Matrix();
matrix.postRotate(orientation);
exif.resetOrientation();
return transform(bitmap, matrix);
} else {
return bitmap;
}
}
private static int getOrientation(final Context c, final Uri imageUri) throws IOException {
int result = 0;
try (InputStream iStream = c.getContentResolver().openInputStream(imageUri)) {
final ExifInterface exifInterface = new ExifInterface(iStream);
final int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
if (orientation == ExifInterface.ORIENTATION_ROTATE_90) {
result = 90;
} else if (orientation == ExifInterface.ORIENTATION_ROTATE_180) {
result = 180;
} else if (orientation == ExifInterface.ORIENTATION_ROTATE_270) {
result = 270;
}
}
return result;
}
public static ExifWrapper getExifData(final Context c, final Bitmap bitmap, final Uri imageUri) {
InputStream stream = null;
try {
stream = c.getContentResolver().openInputStream(imageUri);
final ExifInterface exifInterface = new ExifInterface(stream);
return new ExifWrapper(exifInterface);
} catch (IOException ex) {
Logger.error("Error loading exif data from image", ex);
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException ignored) {}
}
}
return new ExifWrapper(null);
}
}