diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1594d12 --- /dev/null +++ b/CONTRIBUTING.md @@ -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! diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 531019e..836bc27 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -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 diff --git a/doc/index.md b/doc/index.md index c36d6cb..edd881f 100644 --- a/doc/index.md +++ b/doc/index.md @@ -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 diff --git a/plugin.xml b/plugin.xml index b2db93e..a51f172 100644 --- a/plugin.xml +++ b/plugin.xml @@ -1,9 +1,28 @@ + + version="0.2.10-dev"> Camera Cordova Camera Plugin Apache 2.0 diff --git a/src/android/CameraLauncher.java b/src/android/CameraLauncher.java index 57878ab..96c15da 100755 --- a/src/android/CameraLauncher.java +++ b/src/android/CameraLauncher.java @@ -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 diff --git a/src/ios/CDVCamera.m b/src/ios/CDVCamera.m index 42c5237..8c11e60 100644 --- a/src/ios/CDVCamera.m +++ b/src/ios/CDVCamera.m @@ -24,6 +24,7 @@ #import #import #import +#import #import #import #import @@ -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; diff --git a/src/ios/CDVJpegHeaderWriter.m b/src/ios/CDVJpegHeaderWriter.m index 93cafb8..4d3ea24 100644 --- a/src/ios/CDVJpegHeaderWriter.m +++ b/src/ios/CDVJpegHeaderWriter.m @@ -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) { diff --git a/src/windows8/CameraProxy.js b/src/windows8/CameraProxy.js index 65ebdf5..d46553b 100644 --- a/src/windows8/CameraProxy.js +++ b/src/windows8/CameraProxy.js @@ -351,4 +351,4 @@ module.exports = { } }; -require("cordova/windows8/commandProxy").add("Camera",module.exports); +require("cordova/exec/proxy").add("Camera",module.exports); diff --git a/src/wp/Camera.cs b/src/wp/Camera.cs index 5ff8045..b76929b 100644 --- a/src/wp/Camera.cs +++ b/src/wp/Camera.cs @@ -113,8 +113,6 @@ namespace WPCordovaClassLib.Cordova.Commands [DataMember(IsRequired = false, Name = "correctOrientation")] public bool CorrectOrientation { get; set; } - - /// /// Ignored /// @@ -176,16 +174,6 @@ namespace WPCordovaClassLib.Cordova.Commands } } - /// - /// Used to open photo library - /// - PhotoChooserTask photoChooserTask; - - /// - /// Used to open camera application - /// - CameraCaptureTask cameraTask; - /// /// Camera options /// @@ -198,54 +186,70 @@ namespace WPCordovaClassLib.Cordova.Commands string[] args = JSON.JsonHelper.Deserialize(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(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 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; + 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; } } - + /// /// Returns image content in a form of base64 string /// @@ -385,23 +339,29 @@ namespace WPCordovaClassLib.Cordova.Commands /// Base64 representation of the image 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); } /// @@ -410,51 +370,87 @@ namespace WPCordovaClassLib.Cordova.Commands /// Image stream /// File data /// resized image - 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; } + /// + /// Util: Dispose a bitmap resource + /// + /// BitmapSource subclass to dispose + private void DisposeImage(BitmapSource image) + { + if (image != null) + { + try + { + using (var ms = new MemoryStream(new byte[] { 0x0 })) + { + image.SetSource(ms); + } + } + catch (Exception) + { + } + } + } + /// /// Saves captured image in isolated storage /// /// image file name /// Image path - 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(); + } } }