This commit is contained in:
ldeluca 2014-05-05 09:59:51 -04:00
commit 7e9f099301
9 changed files with 416 additions and 220 deletions

16
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,16 @@
# Contributing to Apache Cordova
Anyone can contribute to Cordova. And we need your contributions.
There are multiple ways to contribute: report bugs, improve the docs, and
contribute code.
For instructions on this, start with the
[contribution overview](http://cordova.apache.org/#contribute).
The details are explained there, but the important items are:
- Sign and submit an Apache ICLA (Contributor License Agreement).
- Have a Jira issue open that corresponds to your contribution.
- Run the tests so your patch doesn't break existing functionality.
We look forward to your contributions!

View File

@ -71,3 +71,12 @@
### 0.2.8 (Feb 26, 2014) ### 0.2.8 (Feb 26, 2014)
* CB-1826 Catch OOM on gallery image resize * CB-1826 Catch OOM on gallery image resize
### 0.2.9 (Apr 17, 2014)
* CB-6460: Update license headers
* CB-6422: [windows8] use cordova/exec/proxy
* [WP8] When only targetWidth or targetHeight is provided, use it as the only bound
* CB-4027, CB-5102, CB-2737, CB-2387: [WP] Fix camera issues, cropping, memory leaks
* CB-6212: [iOS] fix warnings compiled under arm64 64-bit
* [BlackBerry10] Add rim xml namespaces declaration
* Add NOTICE file

View File

@ -20,7 +20,7 @@
# org.apache.cordova.camera # org.apache.cordova.camera
This plugin provides an API for taking pictures and for choosing images from This plugin provides an API for taking pictures and for choosing images from
the system's image libarary. the system's image library.
cordova plugin add org.apache.cordova.camera cordova plugin add org.apache.cordova.camera

View File

@ -1,9 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0" <plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:rim="http://www.blackberry.com/ns/widgets"
id="org.apache.cordova.camera" id="org.apache.cordova.camera"
version="0.2.8"> version="0.2.10-dev">
<name>Camera</name> <name>Camera</name>
<description>Cordova Camera Plugin</description> <description>Cordova Camera Plugin</description>
<license>Apache 2.0</license> <license>Apache 2.0</license>

View File

@ -34,16 +34,18 @@ import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import android.app.Activity; import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ContentValues; import android.content.ContentValues;
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.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.Bitmap.CompressFormat;
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.Bundle;
import android.os.Environment; import android.os.Environment;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.util.Base64; import android.util.Base64;
@ -58,7 +60,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
private static final int DATA_URL = 0; // Return base64 encoded string 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 FILE_URI = 1; // Return file uri (content://media/external/images/media/2 for Android)
private static final int NATIVE_URI = 2; // On Android, this is the same as FILE_URI private static final int NATIVE_URI = 2; // On Android, this is the same as FILE_URI
private static final int PHOTOLIBRARY = 0; // Choose image from picture library (same as SAVEDPHOTOALBUM 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 CAMERA = 1; // Take picture from camera
@ -75,6 +77,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
private static final String GET_All = "Get All"; private static final String GET_All = "Get All";
private static final String LOG_TAG = "CameraLauncher"; private static final String LOG_TAG = "CameraLauncher";
private static final int CROP_CAMERA = 100;
private int mQuality; // Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality) 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 targetWidth; // desired width of the image
@ -85,13 +88,14 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
private boolean saveToPhotoAlbum; // Should the picture be saved to the device's photo album private boolean saveToPhotoAlbum; // Should the picture be saved to the device's photo album
private boolean correctOrientation; // Should the pictures orientation be corrected private boolean correctOrientation; // Should the pictures orientation be corrected
private boolean orientationCorrected; // Has the picture's orientation been corrected private boolean orientationCorrected; // Has the picture's orientation been corrected
//private boolean allowEdit; // Should we allow the user to crop the image. UNUSED. private boolean allowEdit; // Should we allow the user to crop the image.
public CallbackContext callbackContext; public CallbackContext callbackContext;
private int numPics; private int numPics;
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;
/** /**
* Executes the request and returns PluginResult. * Executes the request and returns PluginResult.
@ -121,7 +125,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
this.targetHeight = args.getInt(4); this.targetHeight = args.getInt(4);
this.encodingType = args.getInt(5); this.encodingType = args.getInt(5);
this.mediaType = args.getInt(6); this.mediaType = args.getInt(6);
//this.allowEdit = args.getBoolean(7); // This field is unused. this.allowEdit = args.getBoolean(7);
this.correctOrientation = args.getBoolean(8); this.correctOrientation = args.getBoolean(8);
this.saveToPhotoAlbum = args.getBoolean(9); this.saveToPhotoAlbum = args.getBoolean(9);
@ -139,7 +143,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
this.takePicture(destType, encodingType); this.takePicture(destType, encodingType);
} }
else if ((srcType == PHOTOLIBRARY) || (srcType == SAVEDPHOTOALBUM)) { else if ((srcType == PHOTOLIBRARY) || (srcType == SAVEDPHOTOALBUM)) {
this.getImage(srcType, destType); this.getImage(srcType, destType, encodingType);
} }
} }
catch (IllegalArgumentException e) catch (IllegalArgumentException e)
@ -238,33 +242,93 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
* @param quality Compression quality hint (0-100: 0=low quality & high compression, 100=compress of max quality) * @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
*/ */
// TODO: Images selected from SDCARD don't display correctly, but from CAMERA ALBUM do! // TODO: Images selected from SDCARD don't display correctly, but from CAMERA ALBUM do!
public void getImage(int srcType, int returnType) { // TODO: Images from kitkat filechooser not going into crop function
public void getImage(int srcType, int returnType, int encodingType) {
Intent intent = new Intent(); Intent intent = new Intent();
String title = GET_PICTURE; String title = GET_PICTURE;
croppedUri = null;
if (this.mediaType == PICTURE) { if (this.mediaType == PICTURE) {
intent.setType("image/*"); intent.setType("image/*");
if (this.allowEdit) {
intent.setAction(Intent.ACTION_PICK);
intent.putExtra("crop", "true");
if (targetWidth > 0) {
intent.putExtra("outputX", targetWidth);
}
if (targetHeight > 0) {
intent.putExtra("outputY", targetHeight);
}
if (targetHeight > 0 && targetWidth > 0 && targetWidth == targetHeight) {
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
}
File photo = createCaptureFile(encodingType);
croppedUri = Uri.fromFile(photo);
intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, croppedUri);
} else {
intent.setAction(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
}
} else if (this.mediaType == VIDEO) {
intent.setType("video/*");
title = GET_VIDEO;
intent.setAction(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
} else if (this.mediaType == ALLMEDIA) {
// I wanted to make the type 'image/*, video/*' but this does not work on all versions
// of android so I had to go with the wildcard search.
intent.setType("*/*");
title = GET_All;
intent.setAction(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
} }
else if (this.mediaType == VIDEO) {
intent.setType("video/*");
title = GET_VIDEO;
}
else if (this.mediaType == ALLMEDIA) {
// I wanted to make the type 'image/*, video/*' but this does not work on all versions
// of android so I had to go with the wildcard search.
intent.setType("*/*");
title = GET_All;
}
intent.setAction(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
if (this.cordova != null) { if (this.cordova != null) {
this.cordova.startActivityForResult((CordovaPlugin) this, Intent.createChooser(intent, this.cordova.startActivityForResult((CordovaPlugin) this, Intent.createChooser(intent,
new String(title)), (srcType + 1) * 16 + returnType + 1); new String(title)), (srcType + 1) * 16 + returnType + 1);
} }
} }
/**
* Brings up the UI to perform crop on passed image URI
*
* @param picUri
*/
private void performCrop(Uri picUri) {
try {
Intent cropIntent = new Intent("com.android.camera.action.CROP");
// indicate image type and Uri
cropIntent.setDataAndType(picUri, "image/*");
// set crop properties
cropIntent.putExtra("crop", "true");
// indicate output X and Y
if (targetWidth > 0) {
cropIntent.putExtra("outputX", targetWidth);
}
if (targetHeight > 0) {
cropIntent.putExtra("outputY", targetHeight);
}
if (targetHeight > 0 && targetWidth > 0 && targetWidth == targetHeight) {
cropIntent.putExtra("aspectX", 1);
cropIntent.putExtra("aspectY", 1);
}
// retrieve data on return
cropIntent.putExtra("return-data", true);
// start the activity - we handle returning in onActivityResult
if (this.cordova != null) {
this.cordova.startActivityForResult((CordovaPlugin) this,
cropIntent, CROP_CAMERA);
}
} catch (ActivityNotFoundException anfe) {
Log.e(LOG_TAG, "Crop operation not supported on this device");
// Send Uri back to JavaScript for viewing image
this.callbackContext.success(picUri.toString());
}
}
/** /**
* Applies all needed transformation to the image received from the camera. * Applies all needed transformation to the image received from the camera.
* *
@ -355,7 +419,12 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
exif.createOutFile(exifPath); exif.createOutFile(exifPath);
exif.writeExifData(); exif.writeExifData();
} }
if (this.allowEdit) {
performCrop(uri);
} else {
// Send Uri back to JavaScript for viewing image
this.callbackContext.success(uri.toString());
}
} }
// Send Uri back to JavaScript for viewing image // Send Uri back to JavaScript for viewing image
this.callbackContext.success(uri.toString()); this.callbackContext.success(uri.toString());
@ -365,7 +434,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
bitmap = null; bitmap = null;
} }
private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException { private String ouputModifiedBitmap(Bitmap bitmap, Uri uri) throws IOException {
// Create an ExifHelper to save the exif data that is lost during compression // Create an ExifHelper to save the exif data that is lost during compression
String modifiedPath = getTempDirectoryPath() + "/modified.jpg"; String modifiedPath = getTempDirectoryPath() + "/modified.jpg";
@ -392,7 +461,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
return modifiedPath; return modifiedPath;
} }
/** /**
* Applies all needed transformation to the image received from the gallery. * Applies all needed transformation to the image received from the gallery.
* *
* @param destType In which form should we return the image * @param destType In which form should we return the image
@ -400,6 +469,14 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
*/ */
private void processResultFromGallery(int destType, Intent intent) { private void processResultFromGallery(int destType, Intent intent) {
Uri uri = intent.getData(); Uri uri = intent.getData();
if (uri == null) {
if (croppedUri != null) {
uri = croppedUri;
} else {
this.failPicture("null data from photo library");
return;
}
}
int rotate = 0; int rotate = 0;
// If you ask for video or all media type you will automatically get back a file URI // If you ask for video or all media type you will automatically get back a file URI
@ -495,7 +572,50 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
// Get src and dest types from request code // Get src and dest types from request code
int srcType = (requestCode / 16) - 1; int srcType = (requestCode / 16) - 1;
int destType = (requestCode % 16) - 1; int destType = (requestCode % 16) - 1;
// if camera crop
if (requestCode == CROP_CAMERA) {
if (resultCode == Activity.RESULT_OK) {
// // get the returned data
Bundle extras = intent.getExtras();
// get the cropped bitmap
Bitmap thePic = extras.getParcelable("data");
if (thePic == null) {
this.failPicture("Crop returned no data.");
return;
}
// now save the bitmap to a file
OutputStream fOut = null;
File temp_file = new File(getTempDirectoryPath(),
System.currentTimeMillis() + ".jpg");
try {
temp_file.createNewFile();
fOut = new FileOutputStream(temp_file);
thePic.compress(Bitmap.CompressFormat.JPEG, this.mQuality,
fOut);
fOut.flush();
fOut.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
// // Send Uri back to JavaScript for viewing image
this.callbackContext
.success(Uri.fromFile(temp_file).toString());
}// If cancelled
else if (resultCode == Activity.RESULT_CANCELED) {
this.failPicture("Camera cancelled.");
}
// If something else
else {
this.failPicture("Did not complete!");
}
}
// If CAMERA // If CAMERA
if (srcType == CAMERA) { if (srcType == CAMERA) {
// If image available // If image available

View File

@ -24,6 +24,7 @@
#import <Cordova/NSDictionary+Extensions.h> #import <Cordova/NSDictionary+Extensions.h>
#import <ImageIO/CGImageProperties.h> #import <ImageIO/CGImageProperties.h>
#import <AssetsLibrary/ALAssetRepresentation.h> #import <AssetsLibrary/ALAssetRepresentation.h>
#import <AssetsLibrary/AssetsLibrary.h>
#import <ImageIO/CGImageSource.h> #import <ImageIO/CGImageSource.h>
#import <ImageIO/CGImageProperties.h> #import <ImageIO/CGImageProperties.h>
#import <ImageIO/CGImageDestination.h> #import <ImageIO/CGImageDestination.h>
@ -84,7 +85,7 @@ static NSSet* org_apache_cordova_validArrowDirections;
bool hasCamera = [UIImagePickerController isSourceTypeAvailable:sourceType]; bool hasCamera = [UIImagePickerController isSourceTypeAvailable:sourceType];
if (!hasCamera) { if (!hasCamera) {
NSLog(@"Camera.getPicture: source type %d not available.", sourceType); NSLog(@"Camera.getPicture: source type %lu not available.", (unsigned long)sourceType);
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no camera available"]; CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no camera available"];
[self.commandDelegate sendPluginResult:result callbackId:callbackId]; [self.commandDelegate sendPluginResult:result callbackId:callbackId];
return; return;
@ -170,10 +171,10 @@ static NSSet* org_apache_cordova_validArrowDirections;
- (void)displayPopover:(NSDictionary*)options - (void)displayPopover:(NSDictionary*)options
{ {
int x = 0; NSInteger x = 0;
int y = 32; NSInteger y = 32;
int width = 320; NSInteger width = 320;
int height = 480; NSInteger height = 480;
UIPopoverArrowDirection arrowDirection = UIPopoverArrowDirectionAny; UIPopoverArrowDirection arrowDirection = UIPopoverArrowDirectionAny;
if (options) { if (options) {
@ -182,7 +183,7 @@ static NSSet* org_apache_cordova_validArrowDirections;
width = [options integerValueForKey:@"width" defaultValue:320]; width = [options integerValueForKey:@"width" defaultValue:320];
height = [options integerValueForKey:@"height" defaultValue:480]; height = [options integerValueForKey:@"height" defaultValue:480];
arrowDirection = [options integerValueForKey:@"arrowDir" defaultValue:UIPopoverArrowDirectionAny]; arrowDirection = [options integerValueForKey:@"arrowDir" defaultValue:UIPopoverArrowDirectionAny];
if (![org_apache_cordova_validArrowDirections containsObject:[NSNumber numberWithInt:arrowDirection]]) { if (![org_apache_cordova_validArrowDirections containsObject:[NSNumber numberWithUnsignedInteger:arrowDirection]]) {
arrowDirection = UIPopoverArrowDirectionAny; arrowDirection = UIPopoverArrowDirectionAny;
} }
} }
@ -392,7 +393,13 @@ static NSSet* org_apache_cordova_validArrowDirections;
} }
// popoverControllerDidDismissPopover:(id)popoverController is called if popover is cancelled // popoverControllerDidDismissPopover:(id)popoverController is called if popover is cancelled
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no image selected"]; // error callback expects string ATM CDVPluginResult* result;
if ([ALAssetsLibrary authorizationStatus] == ALAuthorizationStatusAuthorized) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"no image selected"]; // error callback expects string ATM
} else {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"has no access to assets"]; // error callback expects string ATM
}
[self.commandDelegate sendPluginResult:result callbackId:cameraPicker.callbackId]; [self.commandDelegate sendPluginResult:result callbackId:cameraPicker.callbackId];
self.hasPendingOperation = NO; self.hasPendingOperation = NO;

View File

@ -190,7 +190,7 @@ const uint mTiffLength = 0x2a; // after byte align bits, next to bits are 0x002a
// construct the complete app1 data block // construct the complete app1 data block
app1 = [[NSMutableString alloc] initWithFormat: @"%@%04x%@%@%@%@%@", app1 = [[NSMutableString alloc] initWithFormat: @"%@%04x%@%@%@%@%@",
app1marker, app1marker,
16 + ([exifIFD length]/2) + ([subExifIFD length]/2) /*16+[exifIFD length]/2*/, (unsigned int)(16 + ([exifIFD length]/2) + ([subExifIFD length]/2)) /*16+[exifIFD length]/2*/,
exifmarker, exifmarker,
tiffheader, tiffheader,
ifd0offset, ifd0offset,
@ -268,10 +268,10 @@ const uint mTiffLength = 0x2a; // after byte align bits, next to bits are 0x002a
} }
// calculate IFD0 terminal offset tags, currently ExifSubIFD // calculate IFD0 terminal offset tags, currently ExifSubIFD
int entrycount = [ifdblock count]; unsigned int entrycount = (unsigned int)[ifdblock count];
if (ifd0flag) { if (ifd0flag) {
// 18 accounts for 8769's width + offset to next ifd, 8 accounts for start of header // 18 accounts for 8769's width + offset to next ifd, 8 accounts for start of header
NSNumber * offset = [NSNumber numberWithInt:[exifstr length] / 2 + [dbstr length] / 2 + 18+8]; NSNumber * offset = [NSNumber numberWithUnsignedInteger:[exifstr length] / 2 + [dbstr length] / 2 + 18+8];
[self appendExifOffsetTagTo: exifstr [self appendExifOffsetTagTo: exifstr
withOffset : offset]; withOffset : offset];
@ -293,7 +293,7 @@ const uint mTiffLength = 0x2a; // after byte align bits, next to bits are 0x002a
NSNumber * dataformat = [formtemplate objectAtIndex:1]; NSNumber * dataformat = [formtemplate objectAtIndex:1];
NSNumber * components = [formtemplate objectAtIndex:2]; NSNumber * components = [formtemplate objectAtIndex:2];
if([components intValue] == 0) { if([components intValue] == 0) {
components = [NSNumber numberWithInt: [data length] * DataTypeToWidth[[dataformat intValue]-1]]; components = [NSNumber numberWithUnsignedInteger:[data length] * DataTypeToWidth[[dataformat intValue]-1]];
} }
return [[NSString alloc] initWithFormat: @"%@%@%08x", return [[NSString alloc] initWithFormat: @"%@%@%08x",
@ -344,7 +344,7 @@ const uint mTiffLength = 0x2a; // after byte align bits, next to bits are 0x002a
[datastr appendString:[dataformat objectAtIndex:3]]; [datastr appendString:[dataformat objectAtIndex:3]];
} }
if ([datastr length] < 8) { if ([datastr length] < 8) {
NSString * format = [NSString stringWithFormat:@"%%0%dd", 8 - [datastr length]]; NSString * format = [NSString stringWithFormat:@"%%0%dd", (int)(8 - [datastr length])];
[datastr appendFormat:format,0]; [datastr appendFormat:format,0];
} }
return datastr; return datastr;
@ -464,7 +464,7 @@ const uint mTiffLength = 0x2a; // after byte align bits, next to bits are 0x002a
-(void) expandContinuedFraction: (NSArray*) fractionlist -(void) expandContinuedFraction: (NSArray*) fractionlist
withResultNumerator: (NSNumber**) numerator withResultNumerator: (NSNumber**) numerator
withResultDenominator: (NSNumber**) denominator { withResultDenominator: (NSNumber**) denominator {
int i = 0; NSUInteger i = 0;
int den = 0; int den = 0;
int num = 0; int num = 0;
if ([fractionlist count] == 1) { if ([fractionlist count] == 1) {

View File

@ -351,4 +351,4 @@ module.exports = {
} }
}; };
require("cordova/windows8/commandProxy").add("Camera",module.exports); require("cordova/exec/proxy").add("Camera",module.exports);

View File

@ -113,8 +113,6 @@ namespace WPCordovaClassLib.Cordova.Commands
[DataMember(IsRequired = false, Name = "correctOrientation")] [DataMember(IsRequired = false, Name = "correctOrientation")]
public bool CorrectOrientation { get; set; } public bool CorrectOrientation { get; set; }
/// <summary> /// <summary>
/// Ignored /// Ignored
/// </summary> /// </summary>
@ -176,16 +174,6 @@ namespace WPCordovaClassLib.Cordova.Commands
} }
} }
/// <summary>
/// Used to open photo library
/// </summary>
PhotoChooserTask photoChooserTask;
/// <summary>
/// Used to open camera application
/// </summary>
CameraCaptureTask cameraTask;
/// <summary> /// <summary>
/// Camera options /// Camera options
/// </summary> /// </summary>
@ -198,54 +186,70 @@ namespace WPCordovaClassLib.Cordova.Commands
string[] args = JSON.JsonHelper.Deserialize<string[]>(options); string[] args = JSON.JsonHelper.Deserialize<string[]>(options);
// ["quality", "destinationType", "sourceType", "targetWidth", "targetHeight", "encodingType", // ["quality", "destinationType", "sourceType", "targetWidth", "targetHeight", "encodingType",
// "mediaType", "allowEdit", "correctOrientation", "saveToPhotoAlbum" ] // "mediaType", "allowEdit", "correctOrientation", "saveToPhotoAlbum" ]
this.cameraOptions = new CameraOptions(); cameraOptions = new CameraOptions();
this.cameraOptions.Quality = int.Parse(args[0]); cameraOptions.Quality = int.Parse(args[0]);
this.cameraOptions.DestinationType = int.Parse(args[1]); cameraOptions.DestinationType = int.Parse(args[1]);
this.cameraOptions.PictureSourceType = int.Parse(args[2]); cameraOptions.PictureSourceType = int.Parse(args[2]);
this.cameraOptions.TargetWidth = int.Parse(args[3]); cameraOptions.TargetWidth = int.Parse(args[3]);
this.cameraOptions.TargetHeight = int.Parse(args[4]); cameraOptions.TargetHeight = int.Parse(args[4]);
this.cameraOptions.EncodingType = int.Parse(args[5]); cameraOptions.EncodingType = int.Parse(args[5]);
this.cameraOptions.MediaType = int.Parse(args[6]); cameraOptions.MediaType = int.Parse(args[6]);
this.cameraOptions.AllowEdit = bool.Parse(args[7]); cameraOptions.AllowEdit = bool.Parse(args[7]);
this.cameraOptions.CorrectOrientation = bool.Parse(args[8]); cameraOptions.CorrectOrientation = bool.Parse(args[8]);
this.cameraOptions.SaveToPhotoAlbum = bool.Parse(args[9]); cameraOptions.SaveToPhotoAlbum = bool.Parse(args[9]);
//this.cameraOptions = String.IsNullOrEmpty(options) ? // a very large number will force the other value to be the bound
// new CameraOptions() : JSON.JsonHelper.Deserialize<CameraOptions>(options); if (cameraOptions.TargetWidth > -1 && cameraOptions.TargetHeight == -1)
{
cameraOptions.TargetHeight = 100000;
}
else if (cameraOptions.TargetHeight > -1 && cameraOptions.TargetWidth == -1)
{
cameraOptions.TargetWidth = 100000;
}
} }
catch (Exception ex) catch (Exception ex)
{ {
this.DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, ex.Message)); DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, ex.Message));
return; return;
} }
//TODO Check if all the options are acceptable if(cameraOptions.DestinationType != Camera.FILE_URI && cameraOptions.DestinationType != Camera.DATA_URL )
{
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Incorrect option: destinationType"));
return;
}
ChooserBase<PhotoResult> chooserTask = null;
if (cameraOptions.PictureSourceType == CAMERA) if (cameraOptions.PictureSourceType == CAMERA)
{ {
cameraTask = new CameraCaptureTask(); chooserTask = new CameraCaptureTask();
cameraTask.Completed += onCameraTaskCompleted; }
cameraTask.Show(); else if ((cameraOptions.PictureSourceType == PHOTOLIBRARY) || (cameraOptions.PictureSourceType == SAVEDPHOTOALBUM))
{
chooserTask = new PhotoChooserTask();
}
// if chooserTask is still null, then PictureSourceType was invalid
if (chooserTask != null)
{
chooserTask.Completed += onTaskCompleted;
chooserTask.Show();
} }
else else
{ {
if ((cameraOptions.PictureSourceType == PHOTOLIBRARY) || (cameraOptions.PictureSourceType == SAVEDPHOTOALBUM)) Debug.WriteLine("Unrecognized PictureSourceType :: " + cameraOptions.PictureSourceType.ToString());
{ DispatchCommandResult(new PluginResult(PluginResult.Status.NO_RESULT));
photoChooserTask = new PhotoChooserTask();
photoChooserTask.Completed += onPickerTaskCompleted;
photoChooserTask.Show();
}
else
{
DispatchCommandResult(new PluginResult(PluginResult.Status.NO_RESULT));
}
} }
} }
public void onCameraTaskCompleted(object sender, PhotoResult e) public void onTaskCompleted(object sender, PhotoResult e)
{ {
var task = sender as ChooserBase<PhotoResult>;
if (task != null)
{
task.Completed -= onTaskCompleted;
}
if (e.Error != null) if (e.Error != null)
{ {
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR)); DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR));
@ -259,119 +263,69 @@ namespace WPCordovaClassLib.Cordova.Commands
{ {
string imagePathOrContent = string.Empty; string imagePathOrContent = string.Empty;
if (cameraOptions.DestinationType == FILE_URI) // Save image back to media library
// only save to photoalbum if it didn't come from there ...
if (cameraOptions.PictureSourceType == CAMERA && cameraOptions.SaveToPhotoAlbum)
{ {
// Save image in media library MediaLibrary library = new MediaLibrary();
if (cameraOptions.SaveToPhotoAlbum) Picture pict = library.SavePicture(e.OriginalFileName, e.ChosenPhoto); // to save to photo-roll ...
{
MediaLibrary library = new MediaLibrary();
Picture pict = library.SavePicture(e.OriginalFileName, e.ChosenPhoto); // to save to photo-roll ...
}
int orient = ImageExifHelper.getImageOrientationFromStream(e.ChosenPhoto);
int newAngle = 0;
switch (orient)
{
case ImageExifOrientation.LandscapeLeft:
newAngle = 90;
break;
case ImageExifOrientation.PortraitUpsideDown:
newAngle = 180;
break;
case ImageExifOrientation.LandscapeRight:
newAngle = 270;
break;
case ImageExifOrientation.Portrait:
default: break; // 0 default already set
}
Stream rotImageStream = ImageExifHelper.RotateStream(e.ChosenPhoto, newAngle);
// we should return stream position back after saving stream to media library
rotImageStream.Seek(0, SeekOrigin.Begin);
WriteableBitmap image = PictureDecoder.DecodeJpeg(rotImageStream);
imagePathOrContent = this.SaveImageToLocalStorage(image, Path.GetFileName(e.OriginalFileName));
} }
else if (cameraOptions.DestinationType == DATA_URL)
int orient = ImageExifHelper.getImageOrientationFromStream(e.ChosenPhoto);
int newAngle = 0;
switch (orient)
{ {
imagePathOrContent = this.GetImageContent(e.ChosenPhoto); case ImageExifOrientation.LandscapeLeft:
newAngle = 90;
break;
case ImageExifOrientation.PortraitUpsideDown:
newAngle = 180;
break;
case ImageExifOrientation.LandscapeRight:
newAngle = 270;
break;
case ImageExifOrientation.Portrait:
default: break; // 0 default already set
} }
else
if (newAngle != 0)
{ {
// TODO: shouldn't this happen before we launch the camera-picker? using (Stream rotImageStream = ImageExifHelper.RotateStream(e.ChosenPhoto, newAngle))
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Incorrect option: destinationType")); {
return; // we should reset stream position after saving stream to media library
rotImageStream.Seek(0, SeekOrigin.Begin);
if (cameraOptions.DestinationType == DATA_URL)
{
imagePathOrContent = GetImageContent(rotImageStream);
}
else // FILE_URL
{
imagePathOrContent = SaveImageToLocalStorage(rotImageStream, Path.GetFileName(e.OriginalFileName));
}
}
}
else // no need to reorient
{
if (cameraOptions.DestinationType == DATA_URL)
{
imagePathOrContent = GetImageContent(e.ChosenPhoto);
}
else // FILE_URL
{
imagePathOrContent = SaveImageToLocalStorage(e.ChosenPhoto, Path.GetFileName(e.OriginalFileName));
}
} }
DispatchCommandResult(new PluginResult(PluginResult.Status.OK, imagePathOrContent)); DispatchCommandResult(new PluginResult(PluginResult.Status.OK, imagePathOrContent));
} }
catch (Exception) catch (Exception)
{ {
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Error retrieving image.")); DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Error retrieving image."));
} }
break; break;
case TaskResult.Cancel: case TaskResult.Cancel:
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Selection cancelled.")); DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Selection cancelled."));
break; break;
default:
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Selection did not complete!"));
break;
}
}
public void onPickerTaskCompleted(object sender, PhotoResult e)
{
if (e.Error != null)
{
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR));
return;
}
switch (e.TaskResult)
{
case TaskResult.OK:
try
{
string imagePathOrContent = string.Empty;
if (cameraOptions.DestinationType == FILE_URI)
{
WriteableBitmap image = PictureDecoder.DecodeJpeg(e.ChosenPhoto);
imagePathOrContent = this.SaveImageToLocalStorage(image, Path.GetFileName(e.OriginalFileName));
}
else if (cameraOptions.DestinationType == DATA_URL)
{
imagePathOrContent = this.GetImageContent(e.ChosenPhoto);
}
else
{
// TODO: shouldn't this happen before we launch the camera-picker?
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Incorrect option: destinationType"));
return;
}
DispatchCommandResult(new PluginResult(PluginResult.Status.OK, imagePathOrContent));
}
catch (Exception)
{
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Error retrieving image."));
}
break;
case TaskResult.Cancel:
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Selection cancelled."));
break;
default: default:
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Selection did not complete!")); DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Selection did not complete!"));
break; break;
@ -385,23 +339,29 @@ namespace WPCordovaClassLib.Cordova.Commands
/// <returns>Base64 representation of the image</returns> /// <returns>Base64 representation of the image</returns>
private string GetImageContent(Stream stream) private string GetImageContent(Stream stream)
{ {
int streamLength = (int)stream.Length; byte[] imageContent = null;
byte[] fileData = new byte[streamLength + 1];
stream.Read(fileData, 0, streamLength);
//use photo's actual width & height if user doesn't provide width & height try
if (cameraOptions.TargetWidth < 0 && cameraOptions.TargetHeight < 0)
{ {
stream.Close(); //use photo's actual width & height if user doesn't provide width & height
return Convert.ToBase64String(fileData); if (cameraOptions.TargetWidth < 0 && cameraOptions.TargetHeight < 0)
{
int streamLength = (int)stream.Length;
imageContent = new byte[streamLength + 1];
stream.Read(imageContent, 0, streamLength);
}
else
{
// resize photo
imageContent = ResizePhoto(stream);
}
} }
else finally
{ {
// resize photo stream.Dispose();
byte[] resizedFile = ResizePhoto(stream, fileData);
stream.Close();
return Convert.ToBase64String(resizedFile);
} }
return Convert.ToBase64String(imageContent);
} }
/// <summary> /// <summary>
@ -410,51 +370,87 @@ namespace WPCordovaClassLib.Cordova.Commands
/// <param name="stream">Image stream</param> /// <param name="stream">Image stream</param>
/// <param name="fileData">File data</param> /// <param name="fileData">File data</param>
/// <returns>resized image</returns> /// <returns>resized image</returns>
private byte[] ResizePhoto(Stream stream, byte[] fileData) private byte[] ResizePhoto(Stream stream)
{ {
int streamLength = (int)stream.Length; //output
int intResult = 0;
byte[] resizedFile; byte[] resizedFile;
stream.Read(fileData, 0, streamLength);
BitmapImage objBitmap = new BitmapImage(); BitmapImage objBitmap = new BitmapImage();
MemoryStream objBitmapStream = new MemoryStream(fileData);
MemoryStream objBitmapStreamResized = new MemoryStream();
WriteableBitmap objWB;
objBitmap.SetSource(stream); objBitmap.SetSource(stream);
objWB = new WriteableBitmap(objBitmap); objBitmap.CreateOptions = BitmapCreateOptions.None;
// resize the photo with user defined TargetWidth & TargetHeight WriteableBitmap objWB = new WriteableBitmap(objBitmap);
Extensions.SaveJpeg(objWB, objBitmapStreamResized, cameraOptions.TargetWidth, cameraOptions.TargetHeight, 0, cameraOptions.Quality); objBitmap.UriSource = null;
//Convert the resized stream to a byte array. //Keep proportionally
streamLength = (int)objBitmapStreamResized.Length; double ratio = Math.Min((double)cameraOptions.TargetWidth / objWB.PixelWidth, (double)cameraOptions.TargetHeight / objWB.PixelHeight);
resizedFile = new Byte[streamLength]; //-1 int width = Convert.ToInt32(ratio * objWB.PixelWidth);
objBitmapStreamResized.Position = 0; int height = Convert.ToInt32(ratio * objWB.PixelHeight);
//for some reason we have to set Position to zero, but we don't have to earlier when we get the bytes from the chosen photo...
intResult = objBitmapStreamResized.Read(resizedFile, 0, streamLength); //Hold the result stream
using (MemoryStream objBitmapStreamResized = new MemoryStream())
{
try
{
// resize the photo with user defined TargetWidth & TargetHeight
Extensions.SaveJpeg(objWB, objBitmapStreamResized, width, height, 0, cameraOptions.Quality);
}
finally
{
//Dispose bitmaps immediately, they are memory expensive
DisposeImage(objBitmap);
DisposeImage(objWB);
GC.Collect();
}
//Convert the resized stream to a byte array.
int streamLength = (int)objBitmapStreamResized.Length;
resizedFile = new Byte[streamLength]; //-1
objBitmapStreamResized.Position = 0;
//for some reason we have to set Position to zero, but we don't have to earlier when we get the bytes from the chosen photo...
objBitmapStreamResized.Read(resizedFile, 0, streamLength);
}
return resizedFile; return resizedFile;
} }
/// <summary>
/// Util: Dispose a bitmap resource
/// </summary>
/// <param name="image">BitmapSource subclass to dispose</param>
private void DisposeImage(BitmapSource image)
{
if (image != null)
{
try
{
using (var ms = new MemoryStream(new byte[] { 0x0 }))
{
image.SetSource(ms);
}
}
catch (Exception)
{
}
}
}
/// <summary> /// <summary>
/// Saves captured image in isolated storage /// Saves captured image in isolated storage
/// </summary> /// </summary>
/// <param name="imageFileName">image file name</param> /// <param name="imageFileName">image file name</param>
/// <returns>Image path</returns> /// <returns>Image path</returns>
private string SaveImageToLocalStorage(WriteableBitmap image, string imageFileName) private string SaveImageToLocalStorage(Stream stream, string imageFileName)
{ {
if (image == null) if (stream == null)
{ {
throw new ArgumentNullException("imageBytes"); throw new ArgumentNullException("imageBytes");
} }
try try
{ {
var isoFile = IsolatedStorageFile.GetUserStoreForApplication(); var isoFile = IsolatedStorageFile.GetUserStoreForApplication();
if (!isoFile.DirectoryExists(isoFolder)) if (!isoFile.DirectoryExists(isoFolder))
@ -464,16 +460,41 @@ namespace WPCordovaClassLib.Cordova.Commands
string filePath = System.IO.Path.Combine("///" + isoFolder + "/", imageFileName); string filePath = System.IO.Path.Combine("///" + isoFolder + "/", imageFileName);
using (var stream = isoFile.CreateFile(filePath)) using (IsolatedStorageFileStream outputStream = isoFile.CreateFile(filePath))
{ {
// resize image if Height and Width defined via options BitmapImage objBitmap = new BitmapImage();
if (cameraOptions.TargetHeight > 0 && cameraOptions.TargetWidth > 0) objBitmap.SetSource(stream);
objBitmap.CreateOptions = BitmapCreateOptions.None;
WriteableBitmap objWB = new WriteableBitmap(objBitmap);
objBitmap.UriSource = null;
try
{ {
image.SaveJpeg(stream, cameraOptions.TargetWidth, cameraOptions.TargetHeight, 0, cameraOptions.Quality);
//use photo's actual width & height if user doesn't provide width & height
if (cameraOptions.TargetWidth < 0 && cameraOptions.TargetHeight < 0)
{
objWB.SaveJpeg(outputStream, objWB.PixelWidth, objWB.PixelHeight, 0, cameraOptions.Quality);
}
else
{
//Resize
//Keep proportionally
double ratio = Math.Min((double)cameraOptions.TargetWidth / objWB.PixelWidth, (double)cameraOptions.TargetHeight / objWB.PixelHeight);
int width = Convert.ToInt32(ratio * objWB.PixelWidth);
int height = Convert.ToInt32(ratio * objWB.PixelHeight);
// resize the photo with user defined TargetWidth & TargetHeight
objWB.SaveJpeg(outputStream, width, height, 0, cameraOptions.Quality);
}
} }
else finally
{ {
image.SaveJpeg(stream, image.PixelWidth, image.PixelHeight, 0, cameraOptions.Quality); //Dispose bitmaps immediately, they are memory expensive
DisposeImage(objBitmap);
DisposeImage(objWB);
GC.Collect();
} }
} }
@ -484,6 +505,10 @@ namespace WPCordovaClassLib.Cordova.Commands
//TODO: log or do something else //TODO: log or do something else
throw; throw;
} }
finally
{
stream.Dispose();
}
} }
} }