CB-4078: Fix for orientation/scaling on Android 4.4+ devices

The only way to get rotation for photos in library (Gallery, File
System, Google Drive,etc) is to first create a temporary file from the
provider. Only then can we determine the orientation and scale the
bitmap correctly. By doing it in a central place, it eliminates reading
the inputstream repetitively in the plugin.
This commit is contained in:
swbradshaw 2016-04-27 22:27:37 -04:00
parent d4a55f20ec
commit f2b4eeded0

View File

@ -26,13 +26,11 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URI;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import org.apache.cordova.CallbackContext; import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin; import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaResourceApi;
import org.apache.cordova.LOG; import org.apache.cordova.LOG;
import org.apache.cordova.PermissionHelper; import org.apache.cordova.PermissionHelper;
import org.apache.cordova.PluginResult; import org.apache.cordova.PluginResult;
@ -40,27 +38,30 @@ import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import android.Manifest; import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity; import android.app.Activity;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat; import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.media.ExifInterface;
import android.media.MediaScannerConnection; import android.media.MediaScannerConnection;
import android.media.MediaScannerConnection.MediaScannerConnectionClient; import android.media.MediaScannerConnection.MediaScannerConnectionClient;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.provider.DocumentsContract;
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.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
/** /**
* This class launches the camera view, allows the user to take a picture, closes the camera view, * This class launches the camera view, allows the user to take a picture, closes the camera view,
@ -117,6 +118,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
private MediaScannerConnection conn; // Used to update gallery app with newly-written files private MediaScannerConnection conn; // Used to update gallery app with newly-written files
private Uri scanMe; // Uri of image to be added to content store private Uri scanMe; // Uri of image to be added to content store
private Uri croppedUri; private Uri croppedUri;
private ExifHelper exifData; // Exif data from source
/** /**
@ -230,8 +232,8 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
* or to display URI in an img tag * or to display URI in an img tag
* img.src=result; * img.src=result;
* *
* @param quality Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality)
* @param returnType Set the type of image to return. * @param returnType Set the type of image to return.
* @param encodingType JPEG or PNG
*/ */
public void callTakePicture(int returnType, int encodingType) { public void callTakePicture(int returnType, int encodingType) {
boolean saveAlbumPermission = PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE); boolean saveAlbumPermission = PermissionHelper.hasPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
@ -339,7 +341,6 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
/** /**
* Get image from photo library. * Get image from photo library.
* *
* @param quality Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality)
* @param srcType The album to get image from. * @param srcType The album to get image from.
* @param returnType Set the type of image to return. * @param returnType Set the type of image to return.
* @param encodingType * @param encodingType
@ -472,7 +473,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
// in the gallery and the modified image is saved in the temporary // in the gallery and the modified image is saved in the temporary
// directory // directory
if (this.saveToPhotoAlbum) { if (this.saveToPhotoAlbum) {
galleryUri = Uri.fromFile(new File(getPicutresPath())); galleryUri = Uri.fromFile(new File(getPicturesPath()));
if(this.allowEdit && this.croppedUri != null) { if(this.allowEdit && this.croppedUri != null) {
writeUncompressedImage(this.croppedUri, galleryUri); writeUncompressedImage(this.croppedUri, galleryUri);
@ -485,7 +486,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
// If sending base64 image back // If sending base64 image back
if (destType == DATA_URL) { if (destType == DATA_URL) {
bitmap = getScaledBitmap(sourcePath); bitmap = getScaledAndRotatedBitmap(sourcePath);
if (bitmap == null) { if (bitmap == null) {
// Try to get the bitmap from intent. // Try to get the bitmap from intent.
@ -499,9 +500,6 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
return; return;
} }
if (rotate != 0 && this.correctOrientation) {
bitmap = getRotatedBitmap(rotate, bitmap, exif);
}
this.processPicture(bitmap, this.encodingType); this.processPicture(bitmap, this.encodingType);
@ -533,7 +531,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
} }
} else { } else {
Uri uri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + "")); Uri uri = Uri.fromFile(createCaptureFile(this.encodingType, System.currentTimeMillis() + ""));
bitmap = getScaledBitmap(sourcePath); bitmap = getScaledAndRotatedBitmap(sourcePath);
// Double-check the bitmap. // Double-check the bitmap.
if (bitmap == null) { if (bitmap == null) {
@ -542,9 +540,6 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
return; return;
} }
if (rotate != 0 && this.correctOrientation) {
bitmap = getRotatedBitmap(rotate, bitmap, exif);
}
// Add compressed version of captured image to returned media store Uri // Add compressed version of captured image to returned media store Uri
OutputStream os = this.cordova.getActivity().getContentResolver().openOutputStream(uri); OutputStream os = this.cordova.getActivity().getContentResolver().openOutputStream(uri);
@ -575,7 +570,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
bitmap = null; bitmap = null;
} }
private String getPicutresPath() private String getPicturesPath()
{ {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "IMG_" + timeStamp + (this.encodingType == JPEG ? ".jpg" : ".png"); String imageFileName = "IMG_" + timeStamp + (this.encodingType == JPEG ? ".jpg" : ".png");
@ -593,15 +588,10 @@ private void refreshGallery(Uri contentUri)
} }
private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException { private String outputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
// Some content: URIs do not map to file paths (e.g. picasa).
String realPath = FileHelper.getRealPath(uri, this.cordova);
// Get filename from uri
String fileName = realPath != null ?
realPath.substring(realPath.lastIndexOf('/') + 1) :
"modified." + (this.encodingType == JPEG ? "jpg" : "png");
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String fileName = "IMG_" + timeStamp + (this.encodingType == JPEG ? ".jpg" : ".png");
String modifiedPath = getTempDirectoryPath() + "/" + fileName; String modifiedPath = getTempDirectoryPath() + "/" + fileName;
OutputStream os = new FileOutputStream(modifiedPath); OutputStream os = new FileOutputStream(modifiedPath);
@ -612,18 +602,14 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
bitmap.compress(compressFormat, this.mQuality, os); bitmap.compress(compressFormat, this.mQuality, os);
os.close(); os.close();
if (realPath != null && this.encodingType == JPEG) { if (exifData != null && this.encodingType == JPEG) {
// Create an ExifHelper to save the exif data that is lost during compression
ExifHelper exif = new ExifHelper();
try { try {
exif.createInFile(realPath);
exif.readExifData();
if (this.correctOrientation && this.orientationCorrected) { if (this.correctOrientation && this.orientationCorrected) {
exif.resetOrientation(); exifData.resetOrientation();
} }
exif.createOutFile(modifiedPath); exifData.createOutFile(modifiedPath);
exif.writeExifData(); exifData.writeExifData();
exifData = null;
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -677,7 +663,7 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
} }
Bitmap bitmap = null; Bitmap bitmap = null;
try { try {
bitmap = getScaledBitmap(uriString); bitmap = getScaledAndRotatedBitmap(uriString);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
@ -687,20 +673,6 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
return; return;
} }
if (this.correctOrientation) {
rotate = getImageOrientation(uri);
if (rotate != 0) {
Matrix matrix = new Matrix();
matrix.setRotate(rotate);
try {
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
this.orientationCorrected = true;
} catch (OutOfMemoryError oom) {
this.orientationCorrected = false;
}
}
}
// If sending base64 image back // If sending base64 image back
if (destType == DATA_URL) { if (destType == DATA_URL) {
this.processPicture(bitmap, this.encodingType); this.processPicture(bitmap, this.encodingType);
@ -712,7 +684,7 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
if ( (this.targetHeight > 0 && this.targetWidth > 0) || if ( (this.targetHeight > 0 && this.targetWidth > 0) ||
(this.correctOrientation && this.orientationCorrected) ) { (this.correctOrientation && this.orientationCorrected) ) {
try { try {
String modifiedPath = this.ouputModifiedBitmap(bitmap, uri); String modifiedPath = this.outputModifiedBitmap(bitmap, uri);
// The modified image is cached by the app in order to get around this and not have to delete you // The modified image is cached by the app in order to get around this and not have to delete you
// application cache I'm adding the current system time to the end of the file url. // application cache I'm adding the current system time to the end of the file url.
this.callbackContext.success("file://" + modifiedPath + "?" + System.currentTimeMillis()); this.callbackContext.success("file://" + modifiedPath + "?" + System.currentTimeMillis());
@ -822,69 +794,19 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
} }
} }
private int getImageOrientation(Uri uri) {
int rotate = 0;
String[] cols = { MediaStore.Images.Media.ORIENTATION };
try {
Cursor cursor = cordova.getActivity().getContentResolver().query(uri,
cols, null, null, null);
if (cursor != null) {
cursor.moveToPosition(0);
rotate = cursor.getInt(0);
cursor.close();
}
} catch (Exception e) {
// You can get an IllegalArgumentException if ContentProvider doesn't support querying for orientation.
}
return rotate;
}
/** /**
* Figure out if the bitmap should be rotated. For instance if the picture was taken in * Write an inputstream to local disk
* portrait mode
* *
* @param rotate * @param fis - The InputStream to write
* @param bitmap * @param dest - Destination on disk to write to
* @return rotated bitmap
*/
private Bitmap getRotatedBitmap(int rotate, Bitmap bitmap, ExifHelper exif) {
Matrix matrix = new Matrix();
if (rotate == 180) {
matrix.setRotate(rotate);
} else {
matrix.setRotate(rotate, (float) bitmap.getWidth() / 2, (float) bitmap.getHeight() / 2);
}
try
{
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
exif.resetOrientation();
}
catch (OutOfMemoryError oom)
{
// You can run out of memory if the image is very large:
// http://simonmacdonald.blogspot.ca/2012/07/change-to-camera-code-in-phonegap-190.html
// If this happens, simply do not rotate the image and return it unmodified.
// If you do not catch the OutOfMemoryError, the Android app crashes.
}
return bitmap;
}
/**
* In the special case where the default width, height and quality are unchanged
* we just write the file out to disk saving the expensive Bitmap.compress function.
*
* @param uri
* @throws FileNotFoundException * @throws FileNotFoundException
* @throws IOException * @throws IOException
*/ */
private void writeUncompressedImage(Uri src, Uri dest) throws FileNotFoundException, private void writeUncompressedImage(InputStream fis, Uri dest) throws FileNotFoundException,
IOException { IOException {
FileInputStream fis = null;
OutputStream os = null; OutputStream os = null;
try { try {
fis = new FileInputStream(FileHelper.stripFileProtocol(src.toString()));
os = this.cordova.getActivity().getContentResolver().openOutputStream(dest); os = this.cordova.getActivity().getContentResolver().openOutputStream(dest);
byte[] buffer = new byte[4096]; byte[] buffer = new byte[4096];
int len; int len;
@ -909,6 +831,21 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
} }
} }
} }
/**
* In the special case where the default width, height and quality are unchanged
* we just write the file out to disk saving the expensive Bitmap.compress function.
*
* @param src
* @throws FileNotFoundException
* @throws IOException
*/
private void writeUncompressedImage(Uri src, Uri dest) throws FileNotFoundException,
IOException {
FileInputStream fis = new FileInputStream(FileHelper.stripFileProtocol(src.toString()));
writeUncompressedImage(fis, dest);
}
/** /**
* Create entry in media store for image * Create entry in media store for image
@ -934,15 +871,15 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
} }
/** /**
* Return a scaled bitmap based on the target width and height * Return a scaled and rotated bitmap based on the target width and height
* *
* @param imagePath * @param imageUrl
* @return * @return
* @throws IOException * @throws IOException
*/ */
private Bitmap getScaledBitmap(String imageUrl) throws IOException { private Bitmap getScaledAndRotatedBitmap(String imageUrl) throws IOException {
// If no new width or height were specified return the original bitmap // If no new width or height were specified, and orientation is not needed return the original bitmap
if (this.targetWidth <= 0 && this.targetHeight <= 0) { if (this.targetWidth <= 0 && this.targetHeight <= 0 && !(this.correctOrientation)) {
InputStream fileStream = null; InputStream fileStream = null;
Bitmap image = null; Bitmap image = null;
try { try {
@ -960,12 +897,61 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
return image; return image;
} }
/* Copy the inputstream to a temporary file on the device.
We then use this temporary file to determine the width/height/orientation.
This is the only way to determine the orientation of the photo coming from 3rd party providers (Google Drive, Dropbox,etc)
This also ensures we create a scaled bitmap with the correct orientation
We delete the temporary file once we are done
*/
File localFile = null;
Uri galleryUri = null;
int rotate = 0;
try {
InputStream fileStream = FileHelper.getInputStreamFromUriString(imageUrl, cordova);
if (fileStream != null) {
// Generate a temporary file
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String fileName = "IMG_" + timeStamp + (this.encodingType == JPEG ? ".jpg" : ".png");
localFile = new File(getTempDirectoryPath() + fileName);
galleryUri = Uri.fromFile(localFile);
writeUncompressedImage(fileStream, galleryUri);
try {
String mimeType = FileHelper.getMimeType(imageUrl.toString(), cordova);
if ("image/jpeg".equalsIgnoreCase(mimeType)) {
// ExifInterface doesn't like the file:// prefix
String filePath = galleryUri.toString().replace("file://", "");
// read exifData of source
exifData = new ExifHelper();
exifData.createInFile(filePath);
// Use ExifInterface to pull rotation information
if (this.correctOrientation) {
ExifInterface exif = new ExifInterface(filePath);
rotate = exifToDegrees(exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED));
}
}
} catch (Exception oe) {
LOG.w(LOG_TAG,"Unable to read Exif data: "+ oe.toString());
rotate = 0;
}
}
}
catch (Exception e)
{
LOG.e(LOG_TAG,"Exception while getting input stream: "+ e.toString());
return null;
}
try {
// figure out the original width and height of the image // figure out the original width and height of the image
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; options.inJustDecodeBounds = true;
InputStream fileStream = null; InputStream fileStream = null;
try { try {
fileStream = FileHelper.getInputStreamFromUriString(imageUrl, cordova); fileStream = FileHelper.getInputStreamFromUriString(galleryUri.toString(), cordova);
BitmapFactory.decodeStream(fileStream, null, options); BitmapFactory.decodeStream(fileStream, null, options);
} finally { } finally {
if (fileStream != null) { if (fileStream != null) {
@ -977,21 +963,40 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
} }
} }
//CB-2292: WTF? Why is the width null? //CB-2292: WTF? Why is the width null?
if(options.outWidth == 0 || options.outHeight == 0) if (options.outWidth == 0 || options.outHeight == 0) {
{
return null; return null;
} }
// User didn't specify output dimensions, but they need orientation
if (this.targetWidth <= 0 && this.targetHeight <= 0) {
this.targetWidth = options.outWidth;
this.targetHeight = options.outHeight;
}
// Setup target width/height based on orientation
int rotatedWidth, rotatedHeight;
boolean rotated= false;
if (rotate == 90 || rotate == 270) {
rotatedWidth = options.outHeight;
rotatedHeight = options.outWidth;
rotated = true;
} else {
rotatedWidth = options.outWidth;
rotatedHeight = options.outHeight;
}
// determine the correct aspect ratio // determine the correct aspect ratio
int[] widthHeight = calculateAspectRatio(options.outWidth, options.outHeight); int[] widthHeight = calculateAspectRatio(rotatedWidth, rotatedHeight);
// Load in the smallest bitmap possible that is closest to the size we want // Load in the smallest bitmap possible that is closest to the size we want
options.inJustDecodeBounds = false; options.inJustDecodeBounds = false;
options.inSampleSize = calculateSampleSize(options.outWidth, options.outHeight, this.targetWidth, this.targetHeight); options.inSampleSize = calculateSampleSize(rotatedWidth, rotatedHeight, widthHeight[0], widthHeight[1]);
Bitmap unscaledBitmap = null; Bitmap unscaledBitmap = null;
try { try {
fileStream = FileHelper.getInputStreamFromUriString(imageUrl, cordova); fileStream = FileHelper.getInputStreamFromUriString(galleryUri.toString(), cordova);
unscaledBitmap = BitmapFactory.decodeStream(fileStream, null, options); unscaledBitmap = BitmapFactory.decodeStream(fileStream, null, options);
} finally { } finally {
if (fileStream != null) { if (fileStream != null) {
@ -1006,7 +1011,33 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
return null; return null;
} }
return Bitmap.createScaledBitmap(unscaledBitmap, widthHeight[0], widthHeight[1], true); int scaledWidth = (!rotated) ? widthHeight[0] : widthHeight[1];
int scaledHeight = (!rotated) ? widthHeight[1] : widthHeight[0];
Bitmap scaledBitmap = Bitmap.createScaledBitmap(unscaledBitmap, scaledWidth, scaledHeight, true);
if (scaledBitmap != unscaledBitmap) {
unscaledBitmap.recycle();
unscaledBitmap = null;
}
if (this.correctOrientation && (rotate != 0)) {
Matrix matrix = new Matrix();
matrix.setRotate(rotate);
try {
scaledBitmap = Bitmap.createBitmap(scaledBitmap, 0, 0, scaledBitmap.getWidth(), scaledBitmap.getHeight(), matrix, true);
this.orientationCorrected = true;
} catch (OutOfMemoryError oom) {
this.orientationCorrected = false;
}
}
return scaledBitmap;
}
finally {
// delete the temporary copy
if (localFile != null) {
localFile.delete();
}
}
} }
/** /**
@ -1027,11 +1058,11 @@ private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
} }
// Only the width was specified // Only the width was specified
else if (newWidth > 0 && newHeight <= 0) { else if (newWidth > 0 && newHeight <= 0) {
newHeight = (newWidth * origHeight) / origWidth; newHeight = (int)((double)(newWidth / (double)origWidth) * origHeight);
} }
// only the height was specified // only the height was specified
else if (newWidth <= 0 && newHeight > 0) { else if (newWidth <= 0 && newHeight > 0) {
newWidth = (newHeight * origWidth) / origHeight; newWidth = (int)((double)(newHeight / (double)origHeight) * origWidth);
} }
// If the user specified both a positive width and height // If the user specified both a positive width and height
// (potentially different aspect ratio) then the width or height is // (potentially different aspect ratio) then the width or height is