Merge pull request #1179 from jgfidelis/rncamera-android-improv

Improve android performance with Expo picture resolve implementation.
This commit is contained in:
João Guilherme Fidelis 2018-01-30 11:42:43 -02:00 committed by GitHub
commit 0f1cdf5819
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 93 additions and 320 deletions

View File

@ -1,282 +0,0 @@
package org.reactnative;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.util.Base64;
import android.util.Log;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.Tag;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.facebook.react.bridge.ReadableMap;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
public class MutableImage {
private static final String TAG = "RNCamera";
private final byte[] originalImageData;
private Bitmap currentRepresentation;
private Metadata originalImageMetaData;
private boolean hasBeenReoriented = false;
public MutableImage(byte[] originalImageData) {
this.originalImageData = originalImageData;
this.currentRepresentation = toBitmap(originalImageData);
}
public void mirrorImage() throws ImageMutationFailedException {
Matrix m = new Matrix();
m.preScale(-1, 1);
Bitmap bitmap = Bitmap.createBitmap(
currentRepresentation,
0,
0,
currentRepresentation.getWidth(),
currentRepresentation.getHeight(),
m,
false
);
if (bitmap == null)
throw new ImageMutationFailedException("failed to mirror");
this.currentRepresentation = bitmap;
}
public int getImageWidth() {
return currentRepresentation.getWidth();
}
public int getImageHeight() {
return currentRepresentation.getHeight();
}
public Bitmap getBitmap() { return currentRepresentation; }
public void fixOrientation() throws ImageMutationFailedException {
try {
Metadata metadata = originalImageMetaData();
ExifIFD0Directory exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
if (exifIFD0Directory == null) {
return;
} else if (exifIFD0Directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
int exifOrientation = exifIFD0Directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
if(exifOrientation != 1) {
rotate(exifOrientation);
exifIFD0Directory.setInt(ExifIFD0Directory.TAG_ORIENTATION, 1);
}
}
} catch (ImageProcessingException | IOException | MetadataException e) {
throw new ImageMutationFailedException("failed to fix orientation", e);
}
}
//see http://www.impulseadventure.com/photo/exif-orientation.html
private void rotate(int exifOrientation) throws ImageMutationFailedException {
final Matrix bitmapMatrix = new Matrix();
switch (exifOrientation) {
case 1:
return;//no rotation required
case 2:
bitmapMatrix.postScale(-1, 1);
break;
case 3:
bitmapMatrix.postRotate(180);
break;
case 4:
bitmapMatrix.postRotate(180);
bitmapMatrix.postScale(-1, 1);
break;
case 5:
bitmapMatrix.postRotate(90);
bitmapMatrix.postScale(-1, 1);
break;
case 6:
bitmapMatrix.postRotate(90);
break;
case 7:
bitmapMatrix.postRotate(270);
bitmapMatrix.postScale(-1, 1);
break;
case 8:
bitmapMatrix.postRotate(270);
break;
default:
break;
}
Bitmap transformedBitmap = Bitmap.createBitmap(
currentRepresentation,
0,
0,
currentRepresentation.getWidth(),
currentRepresentation.getHeight(),
bitmapMatrix,
false
);
if (transformedBitmap == null)
throw new ImageMutationFailedException("failed to rotate");
this.currentRepresentation = transformedBitmap;
this.hasBeenReoriented = true;
}
private static Bitmap toBitmap(byte[] data) {
try {
ByteArrayInputStream inputStream = new ByteArrayInputStream(data);
Bitmap photo = BitmapFactory.decodeStream(inputStream);
inputStream.close();
return photo;
} catch (IOException e) {
throw new IllegalStateException("Will not happen", e);
}
}
public String toBase64(int jpegQualityPercent) {
return Base64.encodeToString(toJpeg(currentRepresentation, jpegQualityPercent), Base64.DEFAULT);
}
public void writeDataToFile(File file, ReadableMap options, int jpegQualityPercent) throws IOException {
FileOutputStream fos = new FileOutputStream(file);
fos.write(toJpeg(currentRepresentation, jpegQualityPercent));
fos.close();
try {
ExifInterface exif = new ExifInterface(file.getAbsolutePath());
// copy original exif data to the output exif...
// unfortunately, this Android ExifInterface class doesn't understand all the tags so we lose some
for (Directory directory : originalImageMetaData().getDirectories()) {
for (Tag tag : directory.getTags()) {
int tagType = tag.getTagType();
Object object = directory.getObject(tagType);
exif.setAttribute(tag.getTagName(), object.toString());
}
}
writeLocationExifData(options, exif);
if(hasBeenReoriented)
rewriteOrientation(exif);
exif.saveAttributes();
} catch (ImageProcessingException | IOException e) {
Log.e(TAG, "failed to save exif data", e);
}
}
private void rewriteOrientation(ExifInterface exif) {
exif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(ExifInterface.ORIENTATION_NORMAL));
}
private void writeLocationExifData(ReadableMap options, ExifInterface exif) {
if(!options.hasKey("metadata"))
return;
ReadableMap metadata = options.getMap("metadata");
if (!metadata.hasKey("location"))
return;
ReadableMap location = metadata.getMap("location");
if(!location.hasKey("coords"))
return;
try {
ReadableMap coords = location.getMap("coords");
double latitude = coords.getDouble("latitude");
double longitude = coords.getDouble("longitude");
GPS.writeExifData(latitude, longitude, exif);
} catch (IOException e) {
Log.e(TAG, "Couldn't write location data", e);
}
}
private Metadata originalImageMetaData() throws ImageProcessingException, IOException {
if(this.originalImageMetaData == null) {//this is expensive, don't do it more than once
originalImageMetaData = ImageMetadataReader.readMetadata(
new BufferedInputStream(new ByteArrayInputStream(originalImageData)),
originalImageData.length
);
}
return originalImageMetaData;
}
private static byte[] toJpeg(Bitmap bitmap, int quality) throws OutOfMemoryError {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream);
try {
return outputStream.toByteArray();
} finally {
try {
outputStream.close();
} catch (IOException e) {
Log.e(TAG, "problem compressing jpeg", e);
}
}
}
public static class ImageMutationFailedException extends Exception {
public ImageMutationFailedException(String detailMessage, Throwable throwable) {
super(detailMessage, throwable);
}
public ImageMutationFailedException(String detailMessage) {
super(detailMessage);
}
}
private static class GPS {
public static void writeExifData(double latitude, double longitude, ExifInterface exif) throws IOException {
exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE, toDegreeMinuteSecods(latitude));
exif.setAttribute(ExifInterface.TAG_GPS_LATITUDE_REF, latitudeRef(latitude));
exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE, toDegreeMinuteSecods(longitude));
exif.setAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF, longitudeRef(longitude));
}
private static String latitudeRef(double latitude) {
return latitude < 0.0d ? "S" : "N";
}
private static String longitudeRef(double longitude) {
return longitude < 0.0d ? "W" : "E";
}
private static String toDegreeMinuteSecods(double latitude) {
latitude = Math.abs(latitude);
int degree = (int) latitude;
latitude *= 60;
latitude -= (degree * 60.0d);
int minute = (int) latitude;
latitude *= 60;
latitude -= (minute * 60.0d);
int second = (int) (latitude * 1000.0d);
StringBuffer sb = new StringBuffer();
sb.append(degree);
sb.append("/1,");
sb.append(minute);
sb.append("/1,");
sb.append(second);
sb.append("/1000,");
return sb.toString();
}
}
}

View File

@ -2,11 +2,13 @@ package org.reactnative.camera.tasks;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.media.ExifInterface;
import android.util.Base64;
import org.reactnative.MutableImage;
import org.reactnative.camera.RNCameraViewHelper;
import org.reactnative.camera.utils.RNFileUtils;
@ -27,6 +29,7 @@ public class ResolveTakenPictureAsyncTask extends AsyncTask<Void, Void, Writable
private byte[] mImageData;
private ReadableMap mOptions;
private File mCacheDirectory;
private Bitmap mBitmap;
public ResolveTakenPictureAsyncTask(byte[] imageData, Promise promise, ReadableMap options) {
mPromise = promise;
@ -48,54 +51,106 @@ public class ResolveTakenPictureAsyncTask extends AsyncTask<Void, Void, Writable
@Override
protected WritableMap doInBackground(Void... voids) {
WritableMap response = Arguments.createMap();
ByteArrayInputStream inputStream = new ByteArrayInputStream(mImageData);
ByteArrayInputStream inputStream = null;
try {
MutableImage mutableImage = new MutableImage(mImageData);
mutableImage.mirrorImage();
mutableImage.fixOrientation();
String encoded = mutableImage.toBase64(getQuality());
response.putString("base64", encoded);
response.putInt("width", mutableImage.getImageWidth());
response.putInt("height", mutableImage.getImageHeight());
if (mOptions.hasKey("exif") && mOptions.getBoolean("exif")) {
ExifInterface exifInterface = new ExifInterface(inputStream);
WritableMap exifData = RNCameraViewHelper.getExifData(exifInterface);
response.putMap("exif", exifData);
// we need the stream only for photos from a device
if (mBitmap == null) {
mBitmap = BitmapFactory.decodeByteArray(mImageData, 0, mImageData.length);
inputStream = new ByteArrayInputStream(mImageData);
}
ByteArrayOutputStream imageStream = new ByteArrayOutputStream();
mutableImage.getBitmap().compress(Bitmap.CompressFormat.JPEG, getQuality(), imageStream);
// Write compressed image to file in cache directory
String filePath = writeStreamToFile(imageStream);
File imageFile = new File(filePath);
String fileUri = Uri.fromFile(imageFile).toString();
response.putString("uri", fileUri);
return response;
} catch (Resources.NotFoundException e) {
mPromise.reject(ERROR_TAG, "Documents directory of the app could not be found.", e);
e.printStackTrace();
} catch (MutableImage.ImageMutationFailedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (inputStream != null) {
ExifInterface exifInterface = new ExifInterface(inputStream);
// Get orientation of the image from mImageData via inputStream
int orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED
);
// Rotate the bitmap to the proper orientation if needed
if (orientation != ExifInterface.ORIENTATION_UNDEFINED) {
mBitmap = rotateBitmap(mBitmap, getImageRotation(orientation));
}
// Write Exif data to the response if requested
if (mOptions.hasKey("exif") && mOptions.getBoolean("exif")) {
WritableMap exifData = RNCameraViewHelper.getExifData(exifInterface);
response.putMap("exif", exifData);
}
}
// Upon rotating, write the image's dimensions to the response
response.putInt("width", mBitmap.getWidth());
response.putInt("height", mBitmap.getHeight());
// Cache compressed image in imageStream
ByteArrayOutputStream imageStream = new ByteArrayOutputStream();
mBitmap.compress(Bitmap.CompressFormat.JPEG, getQuality(), imageStream);
// Write compressed image to file in cache directory
String filePath = writeStreamToFile(imageStream);
File imageFile = new File(filePath);
String fileUri = Uri.fromFile(imageFile).toString();
response.putString("uri", fileUri);
// Write base64-encoded image to the response if requested
if (mOptions.hasKey("base64") && mOptions.getBoolean("base64")) {
response.putString("base64", Base64.encodeToString(imageStream.toByteArray(), Base64.DEFAULT));
}
// Cleanup
imageStream.close();
if (inputStream != null) {
inputStream.close();
inputStream = null;
}
return response;
} catch (Resources.NotFoundException e) {
mPromise.reject(ERROR_TAG, "Documents directory of the app could not be found.", e);
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
mPromise.reject(ERROR_TAG, "An unknown I/O exception has occurred.", e);
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
// An exception had to occur, promise has already been rejected. Do not try to resolve it again.
return null;
}
private Bitmap rotateBitmap(Bitmap source, int angle) {
Matrix matrix = new Matrix();
matrix.postRotate(angle);
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);
}
// Get rotation degrees from Exif orientation enum
private int getImageRotation(int orientation) {
int rotationDegrees = 0;
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
rotationDegrees = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
rotationDegrees = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
rotationDegrees = 270;
break;
}
return rotationDegrees;
}
private String writeStreamToFile(ByteArrayOutputStream inputStream) throws IOException {
String outputPath = null;
IOException exception = null;