cordova-android/framework/src/com/phonegap/CameraLauncher.java
macdonst a9c34e65fb Fix for issue #141: EXIF data stripped from captured photos in android
In order to fix this issue I needed to read the EXIF data. Save it to a temporary object then after the bitmap is compressed I open the file and write the saved EXIF data.

Supports the following EXIF fields if they are set in your image:

APERTURE
DATETIME
EXPOSURE_TIME
FLASH
FOCAL_LENGTH
GPS_ALTITUDE
GPS_ALTITUDE_REF
GPS_DATESTAMP
GPS_LATITUDE
GPS_LATITUDE_REF
GPS_LONGITUDE
GPS_LONGITUDE_REF
GPS_PROCESSING_METHOD
GPS_TIMESTAMP
ISO
MAKE
MODEL
ORIENTATION
WHITE_BALANCE
2011-08-23 01:47:10 +08:00

387 lines
16 KiB
Java
Executable File

/*
* PhoneGap is available under *either* the terms of the modified BSD license *or* the
* MIT License (2008). See http://opensource.org/licenses/alphabetical for full text.
*
* Copyright (c) 2005-2010, Nitobi Software Inc.
* Copyright (c) 2010, IBM Corporation
*/
package com.phonegap;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import org.apache.commons.codec.binary.Base64;
import org.json.JSONArray;
import org.json.JSONException;
import com.phonegap.api.Plugin;
import com.phonegap.api.PluginResult;
import android.app.Activity;
import android.content.ContentValues;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.net.Uri;
import android.util.Log;
/**
* This class launches the camera view, allows the user to take a picture, closes the camera view,
* and returns the captured image. When the camera view is closed, the screen displayed before
* the camera view was shown is redisplayed.
*/
public class CameraLauncher extends Plugin {
private static final int DATA_URL = 0; // Return base64 encoded string
private static final int FILE_URI = 1; // Return file uri (content://media/external/images/media/2 for Android)
private static final int PHOTOLIBRARY = 0; // Choose image from picture library (same as SAVEDPHOTOALBUM for Android)
private static final int CAMERA = 1; // Take picture from camera
private static final int SAVEDPHOTOALBUM = 2; // Choose image from picture library (same as PHOTOLIBRARY for Android)
private static final int JPEG = 0; // Take a picture of type JPEG
private static final int PNG = 1; // Take a picture of type PNG
private int mQuality; // Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality)
private int targetWidth; // desired width of the image
private int targetHeight; // desired height of the image
private Uri imageUri;
private int encodingType;
// Uri of captured image
public String callbackId;
/**
* Constructor.
*/
public CameraLauncher() {
}
/**
* Executes the request and returns PluginResult.
*
* @param action The action to execute.
* @param args JSONArry of arguments for the plugin.
* @param callbackId The callback id used when calling back into JavaScript.
* @return A PluginResult object with a status and message.
*/
public PluginResult execute(String action, JSONArray args, String callbackId) {
PluginResult.Status status = PluginResult.Status.OK;
String result = "";
this.callbackId = callbackId;
try {
if (action.equals("takePicture")) {
int destType = DATA_URL;
if (args.length() > 1) {
destType = args.getInt(1);
}
int srcType = CAMERA;
if (args.length() > 2) {
srcType = args.getInt(2);
}
if (args.length() > 3) {
this.targetWidth = args.getInt(3);
}
if (args.length() > 4) {
this.targetHeight = args.getInt(4);
}
this.encodingType = JPEG;
if (args.length() > 5) {
this.encodingType = args.getInt(5);
}
if (srcType == CAMERA) {
this.takePicture(args.getInt(0), destType, encodingType);
}
else if ((srcType == PHOTOLIBRARY) || (srcType == SAVEDPHOTOALBUM)) {
this.getImage(args.getInt(0), srcType, destType);
}
PluginResult r = new PluginResult(PluginResult.Status.NO_RESULT);
r.setKeepCallback(true);
return r;
}
return new PluginResult(status, result);
} catch (JSONException e) {
e.printStackTrace();
return new PluginResult(PluginResult.Status.JSON_EXCEPTION);
}
}
//--------------------------------------------------------------------------
// LOCAL METHODS
//--------------------------------------------------------------------------
/**
* Take a picture with the camera.
* When an image is captured or the camera view is cancelled, the result is returned
* in PhonegapActivity.onActivityResult, which forwards the result to this.onActivityResult.
*
* The image can either be returned as a base64 string or a URI that points to the file.
* To display base64 string in an img tag, set the source to:
* img.src="data:image/jpeg;base64,"+result;
* or to display URI in an img tag
* 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.
*/
public void takePicture(int quality, int returnType, int encodingType) {
this.mQuality = quality;
// Display camera
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
// Specify file so that large image is captured and returned
// TODO: What if there isn't any external storage?
File photo = createCaptureFile(encodingType);
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(photo));
this.imageUri = Uri.fromFile(photo);
this.ctx.startActivityForResult((Plugin) this, intent, (CAMERA+1)*16 + returnType+1);
}
/**
* Create a file in the applications temporary directory based upon the supplied encoding.
*
* @param encodingType of the image to be taken
* @return a File object pointing to the temporary picture
*/
private File createCaptureFile(int encodingType) {
File photo = null;
if (encodingType == JPEG) {
photo = new File(DirectoryManager.getTempDirectoryPath(ctx), "Pic.jpg");
} else {
photo = new File(DirectoryManager.getTempDirectoryPath(ctx), "Pic.png");
}
return photo;
}
/**
* 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 returnType Set the type of image to return.
*/
// TODO: Images selected from SDCARD don't display correctly, but from CAMERA ALBUM do!
public void getImage(int quality, int srcType, int returnType) {
this.mQuality = quality;
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
this.ctx.startActivityForResult((Plugin) this, Intent.createChooser(intent,
new String("Get Picture")), (srcType+1)*16 + returnType + 1);
}
/**
* Scales the bitmap according to the requested size.
*
* @param bitmap The bitmap to scale.
* @return Bitmap A new Bitmap object of the same bitmap after scaling.
*/
public Bitmap scaleBitmap(Bitmap bitmap) {
int newWidth = this.targetWidth;
int newHeight = this.targetHeight;
int origWidth = bitmap.getWidth();
int origHeight = bitmap.getHeight();
// If no new width or height were specified return the original bitmap
if (newWidth <= 0 && newHeight <= 0) {
return bitmap;
}
// Only the width was specified
else if (newWidth > 0 && newHeight <= 0) {
newHeight = (newWidth * origHeight) / origWidth;
}
// only the height was specified
else if (newWidth <= 0 && newHeight > 0) {
newWidth = (newHeight * origWidth) / origHeight;
}
// If the user specified both a positive width and height
// (potentially different aspect ratio) then the width or height is
// scaled so that the image fits while maintaining aspect ratio.
// Alternatively, the specified width and height could have been
// kept and Bitmap.SCALE_TO_FIT specified when scaling, but this
// would result in whitespace in the new image.
else {
double newRatio = newWidth / (double)newHeight;
double origRatio = origWidth / (double)origHeight;
if (origRatio > newRatio) {
newHeight = (newWidth * origHeight) / origWidth;
} else if (origRatio < newRatio) {
newWidth = (newHeight * origWidth) / origHeight;
}
}
return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
}
/**
* Called when the camera view exits.
*
* @param requestCode The request code originally supplied to startActivityForResult(),
* allowing you to identify who this result came from.
* @param resultCode The integer result code returned by the child activity through its setResult().
* @param intent An Intent, which can return result data to the caller (various data can be attached to Intent "extras").
*/
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
// Get src and dest types from request code
int srcType = (requestCode/16) - 1;
int destType = (requestCode % 16) - 1;
// If CAMERA
if (srcType == CAMERA) {
// If image available
if (resultCode == Activity.RESULT_OK) {
try {
// Create an ExifHelper to save the exif data that is lost during compression
ExifHelper exif = new ExifHelper();
if (this.encodingType == JPEG) {
exif.createInFile(DirectoryManager.getTempDirectoryPath(ctx) + "/Pic.jpg");
exif.readExifData();
}
// Read in bitmap of captured image
Bitmap bitmap;
try {
bitmap = android.provider.MediaStore.Images.Media.getBitmap(this.ctx.getContentResolver(), imageUri);
} catch (FileNotFoundException e) {
Uri uri = intent.getData();
android.content.ContentResolver resolver = this.ctx.getContentResolver();
bitmap = android.graphics.BitmapFactory.decodeStream(resolver.openInputStream(uri));
}
bitmap = scaleBitmap(bitmap);
// If sending base64 image back
if (destType == DATA_URL) {
this.processPicture(bitmap);
}
// If sending filename back
else if (destType == FILE_URI){
// Create entry in media store for image
// (Don't use insertImage() because it uses default compression setting of 50 - no way to change it)
ContentValues values = new ContentValues();
values.put(android.provider.MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
Uri uri = null;
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException e) {
System.out.println("Can't write to external media storage.");
try {
uri = this.ctx.getContentResolver().insert(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, values);
} catch (UnsupportedOperationException ex) {
System.out.println("Can't write to internal media storage.");
this.failPicture("Error capturing image - no media storage found.");
return;
}
}
// Add compressed version of captured image to returned media store Uri
OutputStream os = this.ctx.getContentResolver().openOutputStream(uri);
bitmap.compress(Bitmap.CompressFormat.JPEG, this.mQuality, os);
os.close();
// Restore exif data to file
if (this.encodingType == JPEG) {
exif.createOutFile(FileUtils.getRealPathFromURI(uri, this.ctx));
exif.writeExifData();
}
// Send Uri back to JavaScript for viewing image
this.success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId);
}
bitmap.recycle();
bitmap = null;
System.gc();
} catch (IOException e) {
e.printStackTrace();
this.failPicture("Error capturing image.");
}
}
// If cancelled
else if (resultCode == Activity.RESULT_CANCELED) {
this.failPicture("Camera cancelled.");
}
// If something else
else {
this.failPicture("Did not complete!");
}
}
// If retrieving photo from library
else if ((srcType == PHOTOLIBRARY) || (srcType == SAVEDPHOTOALBUM)) {
if (resultCode == Activity.RESULT_OK) {
Uri uri = intent.getData();
android.content.ContentResolver resolver = this.ctx.getContentResolver();
// If sending base64 image back
if (destType == DATA_URL) {
try {
Bitmap bitmap = android.graphics.BitmapFactory.decodeStream(resolver.openInputStream(uri));
bitmap = scaleBitmap(bitmap);
this.processPicture(bitmap);
bitmap.recycle();
bitmap = null;
System.gc();
} catch (FileNotFoundException e) {
e.printStackTrace();
this.failPicture("Error retrieving image.");
}
}
// If sending filename back
else if (destType == FILE_URI) {
this.success(new PluginResult(PluginResult.Status.OK, uri.toString()), this.callbackId);
}
}
else if (resultCode == Activity.RESULT_CANCELED) {
this.failPicture("Selection cancelled.");
}
else {
this.failPicture("Selection did not complete!");
}
}
}
/**
* Compress bitmap using jpeg, convert to Base64 encoded string, and return to JavaScript.
*
* @param bitmap
*/
public void processPicture(Bitmap bitmap) {
ByteArrayOutputStream jpeg_data = new ByteArrayOutputStream();
try {
if (bitmap.compress(CompressFormat.JPEG, mQuality, jpeg_data)) {
byte[] code = jpeg_data.toByteArray();
byte[] output = Base64.encodeBase64(code);
String js_out = new String(output);
this.success(new PluginResult(PluginResult.Status.OK, js_out), this.callbackId);
js_out = null;
output = null;
code = null;
}
}
catch(Exception e) {
this.failPicture("Error compressing image.");
}
jpeg_data = null;
}
/**
* Send error message to JavaScript.
*
* @param err
*/
public void failPicture(String err) {
this.error(new PluginResult(PluginResult.Status.ERROR, err), this.callbackId);
}
}