(android): android performance improvements (#644)

* removed re-saving of output file
 * moved image processing onto an async task to allow camera to be used while processing is running
This commit is contained in:
Nick Pomfret 2017-03-24 07:00:56 +00:00 committed by Zack Story
parent 98de15cf9a
commit 9fd9d1f8b1
2 changed files with 371 additions and 279 deletions

View File

@ -0,0 +1,285 @@
package com.lwansbrough.RCTCamera;
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.FileInputStream;
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 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);
rotate(exifOrientation);
}
} catch (ImageProcessingException | IOException | MetadataException e) {
throw new ImageMutationFailedException("failed to fix orientation", e);
}
}
private void rotate(int exifOrientation) throws ImageMutationFailedException {
final Matrix bitmapMatrix = new Matrix();
switch (exifOrientation) {
case 1:
break;
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() {
return Base64.encodeToString(toBytes(currentRepresentation), Base64.DEFAULT);
}
public void writeDataToFile(File file, ReadableMap options) throws IOException {
FileOutputStream fos = new FileOutputStream(file);
fos.write(toBytes(currentRepresentation));
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[] toBytes(Bitmap image) {
byte[] result = null;
try {
result = toJpeg(image, 85);
} catch (OutOfMemoryError e) {
try {
result = toJpeg(image, 70);
} catch (OutOfMemoryError e2) {
e.printStackTrace();
}
}
return result;
}
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

@ -6,23 +6,16 @@
package com.lwansbrough.RCTCamera; package com.lwansbrough.RCTCamera;
import android.content.ContentValues; import android.content.ContentValues;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.hardware.Camera; import android.hardware.Camera;
import android.media.*; import android.media.*;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.os.Environment; import android.os.Environment;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.util.Base64; import android.util.Base64;
import android.util.Log; import android.util.Log;
import android.view.Surface; import android.view.Surface;
import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Metadata;
import com.drew.metadata.MetadataException;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactApplicationContext;
@ -489,130 +482,6 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
return byteArray; return byteArray;
} }
private byte[] saveImage(Bitmap image) {
byte[] result = null;
try {
result = compress(image, 85);
} catch (OutOfMemoryError e) {
try {
result = compress(image, 70);
} catch (OutOfMemoryError e2) {
e.printStackTrace();
}
}
return result;
}
private byte[] mirrorImage(byte[] data) {
Bitmap photo = toBitmap(data);
Matrix m = new Matrix();
m.preScale(-1, 1);
Bitmap mirroredImage = Bitmap.createBitmap(photo, 0, 0, photo.getWidth(), photo.getHeight(), m, false);
return saveImage(mirroredImage);
}
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);
}
}
private byte[] rotate(byte[] data, int exifOrientation) {
final Matrix bitmapMatrix = new Matrix();
switch(exifOrientation)
{
case 1:
break;
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 decodedBitmap = toBitmap(data);
Bitmap transformedBitmap = Bitmap.createBitmap(
decodedBitmap, 0, 0, decodedBitmap.getWidth(), decodedBitmap.getHeight(), bitmapMatrix, false
);
return saveImage(transformedBitmap);
}
private byte[] fixOrientation(byte[] data) {
try {
final Metadata metadata = ImageMetadataReader.readMetadata(
new BufferedInputStream(new ByteArrayInputStream(data)), data.length
);
final ExifIFD0Directory exifIFD0Directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
if (exifIFD0Directory == null) {
return data;
} else if (exifIFD0Directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
final int exifOrientation = exifIFD0Directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
return rotate(data, exifOrientation);
}
return data;
} catch (IOException | ImageProcessingException | MetadataException e) {
Log.e(TAG, "fail to fix orientation", e);
return data;
}
}
private void rewriteOrientation(String path) {
try {
ExifInterface exif = new ExifInterface(path);
exif.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(ExifInterface.ORIENTATION_NORMAL));
exif.saveAttributes();
} catch (IOException e) {
Log.e(TAG, "failed to save exif data", e);
}
}
private byte[] compress(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);
}
}
}
@ReactMethod @ReactMethod
public void capture(final ReadableMap options, final Promise promise) { public void capture(final ReadableMap options, final Promise promise) {
int orientation = options.hasKey("orientation") ? options.getInt("orientation") : RCTCamera.getInstance().getOrientation(); int orientation = options.hasKey("orientation") ? options.getInt("orientation") : RCTCamera.getInstance().getOrientation();
@ -662,87 +531,16 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
Camera.PictureCallback captureCallback = new Camera.PictureCallback() { Camera.PictureCallback captureCallback = new Camera.PictureCallback() {
@Override @Override
public void onPictureTaken(byte[] data, Camera camera) { public void onPictureTaken(final byte[] data, Camera camera) {
if (shouldMirror) {
data = mirrorImage(data);
if (data == null) {
promise.reject("Error mirroring image");
}
}
data = fixOrientation(data);
camera.stopPreview(); camera.stopPreview();
camera.startPreview(); camera.startPreview();
switch (options.getInt("target")) { AsyncTask.execute(new Runnable() {
case RCT_CAMERA_CAPTURE_TARGET_MEMORY: @Override
String encoded = Base64.encodeToString(data, Base64.DEFAULT); public void run() {
WritableMap response = new WritableNativeMap(); processImage(new MutableImage(data), shouldMirror, options, promise);
response.putString("data", encoded);
promise.resolve(response);
break;
case RCT_CAMERA_CAPTURE_TARGET_CAMERA_ROLL: {
File cameraRollFile = getOutputCameraRollFile(MEDIA_TYPE_IMAGE);
if (cameraRollFile == null) {
promise.reject("Error creating media file.");
return;
}
Throwable error = writeDataToFile(data, cameraRollFile);
if (error != null) {
promise.reject(error);
return;
}
writeLocationExifData(cameraRollFile, options);
rewriteOrientation(cameraRollFile.getAbsolutePath());
addToMediaStore(cameraRollFile.getAbsolutePath());
resolve(cameraRollFile, promise);
break;
} }
case RCT_CAMERA_CAPTURE_TARGET_DISK: { });
File pictureFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
if (pictureFile == null) {
promise.reject("Error creating media file.");
return;
}
Throwable error = writeDataToFile(data, pictureFile);
if (error != null) {
promise.reject(error);
return;
}
writeLocationExifData(pictureFile, options);
rewriteOrientation(pictureFile.getAbsolutePath());
resolve(pictureFile, promise);
break;
}
case RCT_CAMERA_CAPTURE_TARGET_TEMP: {
File tempFile = getTempMediaFile(MEDIA_TYPE_IMAGE);
if (tempFile == null) {
promise.reject("Error creating media file.");
return;
}
Throwable error = writeDataToFile(data, tempFile);
if (error != null) {
promise.reject(error);
}
writeLocationExifData(tempFile, options);
rewriteOrientation(tempFile.getAbsolutePath());
resolve(tempFile, promise);
break;
}
}
mSafeToCapture = true; mSafeToCapture = true;
} }
@ -758,26 +556,88 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
} }
} }
private void writeLocationExifData(File cameraRollFile, ReadableMap options) { /**
if(!options.hasKey("metadata")) * synchronized in order to prevent the user crashing the app by taking many photos and them all being processed
return; * concurrently which would blow the memory (esp on smaller devices), and slow things down.
*/
ReadableMap metadata = options.getMap("metadata"); private synchronized void processImage(MutableImage mutableImage, Boolean shouldMirror, ReadableMap options, Promise promise) {
if (!metadata.hasKey("location")) if (shouldMirror) {
return; try {
mutableImage.mirrorImage();
ReadableMap location = metadata.getMap("location"); } catch (MutableImage.ImageMutationFailedException e) {
if(!location.hasKey("coords")) promise.reject("Error mirroring image", e);
return; }
}
try { try {
ReadableMap coords = location.getMap("coords"); mutableImage.fixOrientation();
double latitude = coords.getDouble("latitude"); } catch (MutableImage.ImageMutationFailedException e) {
double longitude = coords.getDouble("longitude"); promise.reject("Error mirroring image", e);
}
GPS.writeExifData(cameraRollFile, latitude, longitude); switch (options.getInt("target")) {
} catch (IOException e) { case RCT_CAMERA_CAPTURE_TARGET_MEMORY:
Log.e(TAG, "Couldn't write location data", e); String encoded = mutableImage.toBase64();
WritableMap response = new WritableNativeMap();
response.putString("data", encoded);
promise.resolve(response);
break;
case RCT_CAMERA_CAPTURE_TARGET_CAMERA_ROLL: {
File cameraRollFile = getOutputCameraRollFile(MEDIA_TYPE_IMAGE);
if (cameraRollFile == null) {
promise.reject("Error creating media file.");
return;
}
try {
mutableImage.writeDataToFile(cameraRollFile, options);
} catch (IOException e) {
promise.reject("failed to save image file", e);
return;
}
addToMediaStore(cameraRollFile.getAbsolutePath());
resolve(cameraRollFile, promise);
break;
}
case RCT_CAMERA_CAPTURE_TARGET_DISK: {
File pictureFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);
if (pictureFile == null) {
promise.reject("Error creating media file.");
return;
}
try {
mutableImage.writeDataToFile(pictureFile, options);
} catch (IOException e) {
promise.reject("failed to save image file", e);
return;
}
resolve(pictureFile, promise);
break;
}
case RCT_CAMERA_CAPTURE_TARGET_TEMP: {
File tempFile = getTempMediaFile(MEDIA_TYPE_IMAGE);
if (tempFile == null) {
promise.reject("Error creating media file.");
return;
}
try {
mutableImage.writeDataToFile(tempFile, options);
} catch (IOException e) {
promise.reject("failed to save image file", e);
return;
}
resolve(tempFile, promise);
break;
}
} }
} }
@ -802,20 +662,6 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
promise.resolve(null != flashModes && !flashModes.isEmpty()); promise.resolve(null != flashModes && !flashModes.isEmpty());
} }
private Throwable writeDataToFile(byte[] data, File file) {
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(data);
fos.close();
} catch (FileNotFoundException e) {
return e;
} catch (IOException e) {
return e;
}
return null;
}
private File getOutputMediaFile(int type) { private File getOutputMediaFile(int type) {
// Get environment directory type id from requested media type. // Get environment directory type id from requested media type.
String environmentDirectoryType; String environmentDirectoryType;
@ -890,7 +736,6 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
MediaScannerConnection.scanFile(_reactContext, new String[] { path }, null, null); MediaScannerConnection.scanFile(_reactContext, new String[] { path }, null, null);
} }
/** /**
* LifecycleEventListener overrides * LifecycleEventListener overrides
*/ */
@ -935,42 +780,4 @@ public class RCTCameraModule extends ReactContextBaseJavaModule
}); });
} }
private static class GPS {
public static void writeExifData(File targetFile, double latitude, double longitude) throws IOException {
ExifInterface exif = new ExifInterface(targetFile.getAbsolutePath());
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));
exif.saveAttributes();
}
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();
}
}
} }