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)
* 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
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

View File

@ -1,9 +1,28 @@
<?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"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:rim="http://www.blackberry.com/ns/widgets"
id="org.apache.cordova.camera"
version="0.2.8">
version="0.2.10-dev">
<name>Camera</name>
<description>Cordova Camera Plugin</description>
<license>Apache 2.0</license>

View File

@ -34,16 +34,18 @@ import org.json.JSONArray;
import org.json.JSONException;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ContentValues;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Bitmap.CompressFormat;
import android.media.MediaScannerConnection;
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
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 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 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 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 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 correctOrientation; // Should the pictures orientation be 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;
private int numPics;
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 croppedUri;
/**
* Executes the request and returns PluginResult.
@ -121,7 +125,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
this.targetHeight = args.getInt(4);
this.encodingType = args.getInt(5);
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.saveToPhotoAlbum = args.getBoolean(9);
@ -139,7 +143,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
this.takePicture(destType, encodingType);
}
else if ((srcType == PHOTOLIBRARY) || (srcType == SAVEDPHOTOALBUM)) {
this.getImage(srcType, destType);
this.getImage(srcType, destType, encodingType);
}
}
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 srcType The album to get image from.
* @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!
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();
String title = GET_PICTURE;
croppedUri = null;
if (this.mediaType == PICTURE) {
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) {
this.cordova.startActivityForResult((CordovaPlugin) this, Intent.createChooser(intent,
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.
*
@ -355,7 +419,12 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
exif.createOutFile(exifPath);
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
this.callbackContext.success(uri.toString());
@ -364,8 +433,8 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
this.cleanup(FILE_URI, this.imageUri, uri, bitmap);
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
String modifiedPath = getTempDirectoryPath() + "/modified.jpg";
@ -392,7 +461,7 @@ public class CameraLauncher extends CordovaPlugin implements MediaScannerConnect
return modifiedPath;
}
/**
/**
* Applies all needed transformation to the image received from the gallery.
*
* @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) {
Uri uri = intent.getData();
if (uri == null) {
if (croppedUri != null) {
uri = croppedUri;
} else {
this.failPicture("null data from photo library");
return;
}
}
int rotate = 0;
// 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
int srcType = (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 (srcType == CAMERA) {
// If image available

View File

@ -24,6 +24,7 @@
#import <Cordova/NSDictionary+Extensions.h>
#import <ImageIO/CGImageProperties.h>
#import <AssetsLibrary/ALAssetRepresentation.h>
#import <AssetsLibrary/AssetsLibrary.h>
#import <ImageIO/CGImageSource.h>
#import <ImageIO/CGImageProperties.h>
#import <ImageIO/CGImageDestination.h>
@ -84,7 +85,7 @@ static NSSet* org_apache_cordova_validArrowDirections;
bool hasCamera = [UIImagePickerController isSourceTypeAvailable:sourceType];
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"];
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
return;
@ -170,10 +171,10 @@ static NSSet* org_apache_cordova_validArrowDirections;
- (void)displayPopover:(NSDictionary*)options
{
int x = 0;
int y = 32;
int width = 320;
int height = 480;
NSInteger x = 0;
NSInteger y = 32;
NSInteger width = 320;
NSInteger height = 480;
UIPopoverArrowDirection arrowDirection = UIPopoverArrowDirectionAny;
if (options) {
@ -182,7 +183,7 @@ static NSSet* org_apache_cordova_validArrowDirections;
width = [options integerValueForKey:@"width" defaultValue:320];
height = [options integerValueForKey:@"height" defaultValue:480];
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;
}
}
@ -392,7 +393,13 @@ static NSSet* org_apache_cordova_validArrowDirections;
}
// 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.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
app1 = [[NSMutableString alloc] initWithFormat: @"%@%04x%@%@%@%@%@",
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,
tiffheader,
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
int entrycount = [ifdblock count];
unsigned int entrycount = (unsigned int)[ifdblock count];
if (ifd0flag) {
// 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
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 * components = [formtemplate objectAtIndex:2];
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",
@ -344,7 +344,7 @@ const uint mTiffLength = 0x2a; // after byte align bits, next to bits are 0x002a
[datastr appendString:[dataformat objectAtIndex:3]];
}
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];
}
return datastr;
@ -464,7 +464,7 @@ const uint mTiffLength = 0x2a; // after byte align bits, next to bits are 0x002a
-(void) expandContinuedFraction: (NSArray*) fractionlist
withResultNumerator: (NSNumber**) numerator
withResultDenominator: (NSNumber**) denominator {
int i = 0;
NSUInteger i = 0;
int den = 0;
int num = 0;
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")]
public bool CorrectOrientation { get; set; }
/// <summary>
/// Ignored
/// </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>
/// Camera options
/// </summary>
@ -198,54 +186,70 @@ namespace WPCordovaClassLib.Cordova.Commands
string[] args = JSON.JsonHelper.Deserialize<string[]>(options);
// ["quality", "destinationType", "sourceType", "targetWidth", "targetHeight", "encodingType",
// "mediaType", "allowEdit", "correctOrientation", "saveToPhotoAlbum" ]
this.cameraOptions = new CameraOptions();
this.cameraOptions.Quality = int.Parse(args[0]);
this.cameraOptions.DestinationType = int.Parse(args[1]);
this.cameraOptions.PictureSourceType = int.Parse(args[2]);
this.cameraOptions.TargetWidth = int.Parse(args[3]);
this.cameraOptions.TargetHeight = int.Parse(args[4]);
this.cameraOptions.EncodingType = int.Parse(args[5]);
this.cameraOptions.MediaType = int.Parse(args[6]);
this.cameraOptions.AllowEdit = bool.Parse(args[7]);
this.cameraOptions.CorrectOrientation = bool.Parse(args[8]);
this.cameraOptions.SaveToPhotoAlbum = bool.Parse(args[9]);
//this.cameraOptions = String.IsNullOrEmpty(options) ?
// new CameraOptions() : JSON.JsonHelper.Deserialize<CameraOptions>(options);
cameraOptions = new CameraOptions();
cameraOptions.Quality = int.Parse(args[0]);
cameraOptions.DestinationType = int.Parse(args[1]);
cameraOptions.PictureSourceType = int.Parse(args[2]);
cameraOptions.TargetWidth = int.Parse(args[3]);
cameraOptions.TargetHeight = int.Parse(args[4]);
cameraOptions.EncodingType = int.Parse(args[5]);
cameraOptions.MediaType = int.Parse(args[6]);
cameraOptions.AllowEdit = bool.Parse(args[7]);
cameraOptions.CorrectOrientation = bool.Parse(args[8]);
cameraOptions.SaveToPhotoAlbum = bool.Parse(args[9]);
// a very large number will force the other value to be the bound
if (cameraOptions.TargetWidth > -1 && cameraOptions.TargetHeight == -1)
{
cameraOptions.TargetHeight = 100000;
}
else if (cameraOptions.TargetHeight > -1 && cameraOptions.TargetWidth == -1)
{
cameraOptions.TargetWidth = 100000;
}
}
catch (Exception ex)
{
this.DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, ex.Message));
DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, ex.Message));
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)
{
cameraTask = new CameraCaptureTask();
cameraTask.Completed += onCameraTaskCompleted;
cameraTask.Show();
chooserTask = new CameraCaptureTask();
}
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
{
if ((cameraOptions.PictureSourceType == PHOTOLIBRARY) || (cameraOptions.PictureSourceType == SAVEDPHOTOALBUM))
{
photoChooserTask = new PhotoChooserTask();
photoChooserTask.Completed += onPickerTaskCompleted;
photoChooserTask.Show();
}
else
{
DispatchCommandResult(new PluginResult(PluginResult.Status.NO_RESULT));
}
Debug.WriteLine("Unrecognized PictureSourceType :: " + cameraOptions.PictureSourceType.ToString());
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)
{
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR));
@ -259,125 +263,75 @@ namespace WPCordovaClassLib.Cordova.Commands
{
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
if (cameraOptions.SaveToPhotoAlbum)
{
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));
MediaLibrary library = new MediaLibrary();
Picture pict = library.SavePicture(e.OriginalFileName, e.ChosenPhoto); // to save to photo-roll ...
}
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?
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Incorrect option: destinationType"));
return;
using (Stream rotImageStream = ImageExifHelper.RotateStream(e.ChosenPhoto, newAngle))
{
// 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));
}
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:
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:
DispatchCommandResult(new PluginResult(PluginResult.Status.ERROR, "Selection did not complete!"));
break;
}
}
/// <summary>
/// Returns image content in a form of base64 string
/// </summary>
@ -385,23 +339,29 @@ namespace WPCordovaClassLib.Cordova.Commands
/// <returns>Base64 representation of the image</returns>
private string GetImageContent(Stream stream)
{
int streamLength = (int)stream.Length;
byte[] fileData = new byte[streamLength + 1];
stream.Read(fileData, 0, streamLength);
byte[] imageContent = null;
//use photo's actual width & height if user doesn't provide width & height
if (cameraOptions.TargetWidth < 0 && cameraOptions.TargetHeight < 0)
try
{
stream.Close();
return Convert.ToBase64String(fileData);
//use photo's actual width & height if user doesn't provide width & height
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
byte[] resizedFile = ResizePhoto(stream, fileData);
stream.Close();
return Convert.ToBase64String(resizedFile);
stream.Dispose();
}
return Convert.ToBase64String(imageContent);
}
/// <summary>
@ -410,51 +370,87 @@ namespace WPCordovaClassLib.Cordova.Commands
/// <param name="stream">Image stream</param>
/// <param name="fileData">File data</param>
/// <returns>resized image</returns>
private byte[] ResizePhoto(Stream stream, byte[] fileData)
private byte[] ResizePhoto(Stream stream)
{
int streamLength = (int)stream.Length;
int intResult = 0;
//output
byte[] resizedFile;
stream.Read(fileData, 0, streamLength);
BitmapImage objBitmap = new BitmapImage();
MemoryStream objBitmapStream = new MemoryStream(fileData);
MemoryStream objBitmapStreamResized = new MemoryStream();
WriteableBitmap objWB;
objBitmap.SetSource(stream);
objWB = new WriteableBitmap(objBitmap);
objBitmap.CreateOptions = BitmapCreateOptions.None;
// resize the photo with user defined TargetWidth & TargetHeight
Extensions.SaveJpeg(objWB, objBitmapStreamResized, cameraOptions.TargetWidth, cameraOptions.TargetHeight, 0, cameraOptions.Quality);
WriteableBitmap objWB = new WriteableBitmap(objBitmap);
objBitmap.UriSource = null;
//Convert the resized stream to a byte array.
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...
intResult = objBitmapStreamResized.Read(resizedFile, 0, streamLength);
//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);
//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;
}
/// <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>
/// Saves captured image in isolated storage
/// </summary>
/// <param name="imageFileName">image file name</param>
/// <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");
}
try
{
var isoFile = IsolatedStorageFile.GetUserStoreForApplication();
if (!isoFile.DirectoryExists(isoFolder))
@ -464,16 +460,41 @@ namespace WPCordovaClassLib.Cordova.Commands
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
if (cameraOptions.TargetHeight > 0 && cameraOptions.TargetWidth > 0)
BitmapImage objBitmap = new BitmapImage();
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
throw;
}
finally
{
stream.Dispose();
}
}
}