Skip to content

Commit

Permalink
(android): android performance improvements (react-native-camera#644)
Browse files Browse the repository at this point in the history
* removed re-saving of output file
 * moved image processing onto an async task to allow camera to be used while processing is running
  • Loading branch information
npomfret authored and rt2zz committed Mar 24, 2017
1 parent 98de15c commit 9fd9d1f
Show file tree
Hide file tree
Showing 2 changed files with 370 additions and 278 deletions.
285 changes: 285 additions & 0 deletions android/src/main/java/com/lwansbrough/RCTCamera/MutableImage.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Loading

0 comments on commit 9fd9d1f

Please sign in to comment.