| /* |
| Licensed 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. |
| */ |
| |
| using System; |
| using System.Net; |
| using System.Windows; |
| using System.Windows.Controls; |
| using System.Windows.Documents; |
| using System.Windows.Ink; |
| using System.Windows.Input; |
| using System.Windows.Media; |
| using System.Windows.Media.Animation; |
| using System.Collections.Generic; |
| using Microsoft.Phone.Tasks; |
| using System.Runtime.Serialization; |
| using System.IO; |
| using System.IO.IsolatedStorage; |
| using System.Windows.Media.Imaging; |
| using Microsoft.Phone; |
| using Microsoft.Xna.Framework.Media; |
| using System.Diagnostics; |
| |
| namespace WPCordovaClassLib.Cordova.Commands |
| { |
| public class Camera : BaseCommand |
| { |
| |
| /// <summary> |
| /// Return base64 encoded string |
| /// </summary> |
| private const int DATA_URL = 0; |
| |
| /// <summary> |
| /// Return file uri |
| /// </summary> |
| private const int FILE_URI = 1; |
| |
| /// <summary> |
| /// Choose image from picture library |
| /// </summary> |
| private const int PHOTOLIBRARY = 0; |
| |
| /// <summary> |
| /// Take picture from camera |
| /// </summary> |
| |
| private const int CAMERA = 1; |
| |
| /// <summary> |
| /// Choose image from picture library |
| /// </summary> |
| private const int SAVEDPHOTOALBUM = 2; |
| |
| /// <summary> |
| /// Take a picture of type JPEG |
| /// </summary> |
| private const int JPEG = 0; |
| |
| /// <summary> |
| /// Take a picture of type PNG |
| /// </summary> |
| private const int PNG = 1; |
| |
| /// <summary> |
| /// Folder to store captured images |
| /// </summary> |
| private const string isoFolder = "CapturedImagesCache"; |
| |
| /// <summary> |
| /// Represents captureImage action options. |
| /// </summary> |
| [DataContract] |
| public class CameraOptions |
| { |
| /// <summary> |
| /// Source to getPicture from. |
| /// </summary> |
| [DataMember(IsRequired = false, Name = "sourceType")] |
| public int PictureSourceType { get; set; } |
| |
| /// <summary> |
| /// Format of image that returned from getPicture. |
| /// </summary> |
| [DataMember(IsRequired = false, Name = "destinationType")] |
| public int DestinationType { get; set; } |
| |
| /// <summary> |
| /// Quality of saved image |
| /// </summary> |
| [DataMember(IsRequired = false, Name = "quality")] |
| public int Quality { get; set; } |
| |
| /// <summary> |
| /// Controls whether or not the image is also added to the device photo album. |
| /// </summary> |
| [DataMember(IsRequired = false, Name = "saveToPhotoAlbum")] |
| public bool SaveToPhotoAlbum { get; set; } |
| |
| /// <summary> |
| /// Ignored |
| /// </summary> |
| [DataMember(IsRequired = false, Name = "correctOrientation")] |
| public bool CorrectOrientation { get; set; } |
| |
| /// <summary> |
| /// Ignored |
| /// </summary> |
| [DataMember(IsRequired = false, Name = "allowEdit")] |
| public bool AllowEdit { get; set; } |
| |
| /// <summary> |
| /// Height in pixels to scale image |
| /// </summary> |
| [DataMember(IsRequired = false, Name = "encodingType")] |
| public int EncodingType { get; set; } |
| |
| /// <summary> |
| /// Height in pixels to scale image |
| /// </summary> |
| [DataMember(IsRequired = false, Name = "mediaType")] |
| public int MediaType { get; set; } |
| |
| |
| /// <summary> |
| /// Height in pixels to scale image |
| /// </summary> |
| [DataMember(IsRequired = false, Name = "targetHeight")] |
| public int TargetHeight { get; set; } |
| |
| |
| /// <summary> |
| /// Width in pixels to scale image |
| /// </summary> |
| [DataMember(IsRequired = false, Name = "targetWidth")] |
| public int TargetWidth { get; set; } |
| |
| /// <summary> |
| /// Creates options object with default parameters |
| /// </summary> |
| public CameraOptions() |
| { |
| this.SetDefaultValues(new StreamingContext()); |
| } |
| |
| /// <summary> |
| /// Initializes default values for class fields. |
| /// Implemented in separate method because default constructor is not invoked during deserialization. |
| /// </summary> |
| /// <param name="context"></param> |
| [OnDeserializing()] |
| public void SetDefaultValues(StreamingContext context) |
| { |
| PictureSourceType = CAMERA; |
| DestinationType = FILE_URI; |
| Quality = 80; |
| TargetHeight = -1; |
| TargetWidth = -1; |
| SaveToPhotoAlbum = false; |
| CorrectOrientation = true; |
| AllowEdit = false; |
| MediaType = -1; |
| EncodingType = -1; |
| } |
| } |
| |
| /// <summary> |
| /// Camera options |
| /// </summary> |
| CameraOptions cameraOptions; |
| |
| public void takePicture(string options) |
| { |
| try |
| { |
| string[] args = JSON.JsonHelper.Deserialize<string[]>(options); |
| // ["quality", "destinationType", "sourceType", "targetWidth", "targetHeight", "encodingType", |
| // "mediaType", "allowEdit", "correctOrientation", "saveToPhotoAlbum" ] |
| 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) |
| { |
| DispatchCommandResult(new PluginResult(PluginResult.Status.JSON_EXCEPTION, ex.Message)); |
| return; |
| } |
| |
| 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) |
| { |
| 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 |
| { |
| Debug.WriteLine("Unrecognized PictureSourceType :: " + cameraOptions.PictureSourceType.ToString()); |
| DispatchCommandResult(new PluginResult(PluginResult.Status.NO_RESULT)); |
| } |
| } |
| |
| 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)); |
| return; |
| } |
| |
| switch (e.TaskResult) |
| { |
| case TaskResult.OK: |
| try |
| { |
| string imagePathOrContent = string.Empty; |
| |
| // Save image back to media library |
| // only save to photoalbum if it didn't come from there ... |
| if (cameraOptions.PictureSourceType == CAMERA && cameraOptions.SaveToPhotoAlbum) |
| { |
| MediaLibrary library = new MediaLibrary(); |
| Picture pict = library.SavePicture(e.OriginalFileName, e.ChosenPhoto); // to save to photo-roll ... |
| } |
| |
| int newAngle = 0; |
| // There's bug in Windows Phone 8.1 causing Seek on a DssPhotoStream not working properly. |
| // https://connect.microsoft.com/VisualStudio/feedback/details/783252 |
| // But a mis-oriented file is better than nothing, so try and catch. |
| try { |
| int orient = ImageExifHelper.getImageOrientationFromStream(e.ChosenPhoto); |
| 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 |
| } |
| } catch { |
| Debug.WriteLine("Error fetching orientation from Exif"); |
| } |
| |
| if (newAngle != 0) |
| { |
| 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; |
| } |
| } |
| |
| /// <summary> |
| /// Returns image content in a form of base64 string |
| /// </summary> |
| /// <param name="stream">Image stream</param> |
| /// <returns>Base64 representation of the image</returns> |
| private string GetImageContent(Stream stream) |
| { |
| byte[] imageContent = null; |
| |
| try |
| { |
| // Resize photo and convert to JPEG |
| imageContent = ResizePhoto(stream); |
| } |
| finally |
| { |
| stream.Dispose(); |
| } |
| |
| return Convert.ToBase64String(imageContent); |
| } |
| |
| /// <summary> |
| /// Resize image |
| /// </summary> |
| /// <param name="stream">Image stream</param> |
| /// <returns>resized image</returns> |
| private byte[] ResizePhoto(Stream stream) |
| { |
| //output |
| byte[] resizedFile; |
| |
| BitmapImage objBitmap = new BitmapImage(); |
| objBitmap.SetSource(stream); |
| objBitmap.CreateOptions = BitmapCreateOptions.None; |
| |
| WriteableBitmap objWB = new WriteableBitmap(objBitmap); |
| objBitmap.UriSource = null; |
| |
| // Calculate resultant image size |
| int width, height; |
| if (cameraOptions.TargetWidth >= 0 && cameraOptions.TargetHeight >= 0) |
| { |
| // Keep proportionally |
| double ratio = Math.Min( |
| (double)cameraOptions.TargetWidth / objWB.PixelWidth, |
| (double)cameraOptions.TargetHeight / objWB.PixelHeight); |
| width = Convert.ToInt32(ratio * objWB.PixelWidth); |
| height = Convert.ToInt32(ratio * objWB.PixelHeight); |
| } |
| else |
| { |
| width = objWB.PixelWidth; |
| height = 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(Stream stream, string imageFileName) |
| { |
| |
| if (stream == null) |
| { |
| throw new ArgumentNullException("imageBytes"); |
| } |
| try |
| { |
| var isoFile = IsolatedStorageFile.GetUserStoreForApplication(); |
| |
| if (!isoFile.DirectoryExists(isoFolder)) |
| { |
| isoFile.CreateDirectory(isoFolder); |
| } |
| |
| string filePath = System.IO.Path.Combine("///" + isoFolder + "/", imageFileName); |
| |
| using (IsolatedStorageFileStream outputStream = isoFile.CreateFile(filePath)) |
| { |
| BitmapImage objBitmap = new BitmapImage(); |
| objBitmap.SetSource(stream); |
| objBitmap.CreateOptions = BitmapCreateOptions.None; |
| |
| WriteableBitmap objWB = new WriteableBitmap(objBitmap); |
| objBitmap.UriSource = null; |
| |
| try |
| { |
| |
| //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); |
| } |
| } |
| finally |
| { |
| //Dispose bitmaps immediately, they are memory expensive |
| DisposeImage(objBitmap); |
| DisposeImage(objWB); |
| GC.Collect(); |
| } |
| } |
| |
| return new Uri(filePath, UriKind.Relative).ToString(); |
| } |
| catch (Exception) |
| { |
| //TODO: log or do something else |
| throw; |
| } |
| finally |
| { |
| stream.Dispose(); |
| } |
| } |
| |
| } |
| } |