I am taking a picture with the iOS camera and trying to extract metadata from the image. This is my code:-
partial void BtnCamera_TouchUpInside(UIButton sender)
{
UIImagePickerController imagePicker = new UIImagePickerController();
imagePicker.PrefersStatusBarHidden();
imagePicker.SourceType = UIImagePickerControllerSourceType.Camera;
// handle saving picture and extracting meta-data from picture //
imagePicker.FinishedPickingMedia += Handle_FinishedPickingMedia;
// present //
PresentViewController(imagePicker, true, () => { });
}
protected void Handle_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
{
try
{
// determine what was selected, video or image
bool isImage = false;
switch (e.Info[UIImagePickerController.MediaType].ToString())
{
case "public.image":
isImage = true;
break;
}
// get common info
NSUrl referenceURL = e.Info[new NSString("UIImagePickerControllerReferenceURL")] as NSUrl;
if (referenceURL != null)
Console.WriteLine("Url:" + referenceURL.ToString());
I am able to initiate the camera, take the picture and then however when I click 'use photo'... The referenceURL comes back as NULL... How can I get the url, such that to extract GPS coordinates of the photo and such other attributes ?
I had a tremendous amount of trouble with the URL. It can be a file, it can be a web url and it acts different on every device. My app crashed and burned so many times with my test group. I finally found a way to get the Metadata from the data. There are multiple ways to get the DateTaken, Width and Height as well as GPS Coordinates. In addition, I needed the Camera MFG and the Model.
string dateTaken = string.Empty;
string lat = string.Empty;
string lon = string.Empty;
string width = string.Empty;
string height = string.Empty;
string mfg = string.Empty;
string model = string.Empty;
PHImageManager.DefaultManager.RequestImageData(asset, options, (data, dataUti, orientation, info) => {
dateTaken = asset.CreationDate.ToString();
// GPS Coordinates
var coord = asset.Location?.Coordinate;
if (coord != null)
{
lat = asset.Location?.Coordinate.Latitude.ToString();
lon = asset.Location?.Coordinate.Longitude.ToString();
}
UIImage img = UIImage.LoadFromData(data);
if (img.CGImage != null)
{
width = img.CGImage?.Width.ToString();
height = img.CGImage?.Height.ToString();
}
using (CGImageSource imageSource = CGImageSource.FromData(data, null))
{
if (imageSource != null)
{
var ns = new NSDictionary();
var imageProperties = imageSource.CopyProperties(ns, 0);
if (imageProperties != null)
{
width = ReturnStringIfNull(imageProperties[CGImageProperties.PixelWidth]);
height = ReturnStringIfNull(imageProperties[CGImageProperties.PixelHeight]);
var tiff = imageProperties.ObjectForKey(CGImageProperties.TIFFDictionary) as NSDictionary;
if (tiff != null)
{
mfg = ReturnStringIfNull(tiff[CGImageProperties.TIFFMake]);
model = ReturnStringIfNull(tiff[CGImageProperties.TIFFModel]);
//dateTaken = ReturnStringIfNull(tiff[CGImageProperties.TIFFDateTime]);
}
}
}
}
}
}
The little helper function
private string ReturnStringIfNull(NSObject inObj)
{
if (inObj == null) return String.Empty;
return inObj.ToString();
}
You can request a PHAsset from the reference Url and that will contain some metadata. You can request the image data to obtain more.
Note: If you need full EXIF, you need to check to ensure the image on on the device (could be iCloud-based), download it if needed, and then load the image data with the ImageIO framework (lots of SO postings cover this).
public void ImagePicker_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
{
void ImageData(PHAsset asset)
{
if (asset == null) throw new Exception("PHAsset is null");
PHImageManager.DefaultManager.RequestImageData(asset, null, (data, dataUti, orientation, info) =>
{
Console.WriteLine(data);
Console.WriteLine(info);
});
}
PHAsset phAsset;
if (e.ReferenceUrl == null)
{
e.OriginalImage?.SaveToPhotosAlbum((image, error) =>
{
if (error == null)
{
var options = new PHFetchOptions
{
FetchLimit = 1,
SortDescriptors = new[] { new NSSortDescriptor("creationDate", true) }
};
phAsset = PHAsset.FetchAssets(options).FirstOrDefault() as PHAsset;
ImageData(phAsset);
}
});
}
else
{
phAsset = PHAsset.FetchAssets(new[] { e.ReferenceUrl }, null).FirstOrDefault() as PHAsset;
ImageData(phAsset);
}
}
Note: Make sure you have request runtime photo library authorization PHPhotoLibrary.RequestAuthorization) and have set the Privacy - Photo Library Usage Description string in your info.plist to avoid a nasty privacy crash
Related
I am using my first Xamarin iOS app to take a picture, extract meta data of the picture, attach the picture to an email body and send email.
// (1.) Take a photo with the Camera //
partial void BtnCamera_TouchUpInside(UIButton sender)
{
UIImagePickerController imagePicker = new UIImagePickerController();
imagePicker.PrefersStatusBarHidden();
imagePicker.SourceType = UIImagePickerControllerSourceType.Camera;
// handle saving picture and extracting meta-data from picture //
imagePicker.FinishedPickingMedia += Handle_FinishedPickingMedia;
// present //
PresentViewController(imagePicker, true, () => { });
}
// (2.) Saves the image to the phone and then extracts metadata //
protected void Handle_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
{
//NSUrl url = null;
try
{
#region Save Image and Get Meta-data
// Save Image before processing for meta-data //
SaveImagetoPhone(e);
// Get meta-data from saved image //
GetImageMetaData(e);
// (3.) Save picture to the phone and extract photo url //
private static void SaveImagetoPhone(UIImagePickerMediaPickedEventArgs e)
{
NSUrl url = null;
void ImageData(PHAsset asset)
{
if (asset == null)
throw new Exception("PHAsset is null");
PHImageManager.DefaultManager.RequestImageData(asset, null, (data, dataUti, orientation, info) =>
{
url = info.ValueForKey(new NSString("PHImageFileURLKey")) as NSUrl;
PhotoDataClass._file = url.Path;
}); }
PHAsset phAsset;
if (e.ReferenceUrl == null)
{
e.OriginalImage?.SaveToPhotosAlbum((image, error) =>
{
if (error == null)
{
var options = new PHFetchOptions
{
FetchLimit = 1,
SortDescriptors = new[] { new NSSortDescriptor("creationDate", true) }
};
phAsset = PHAsset.FetchAssets(options).LastOrDefault() as PHAsset;
ImageData(phAsset);
}
});
}
else
{
phAsset = PHAsset.FetchAssets(new[] { e.ReferenceUrl }, null).FirstOrDefault() as PHAsset;
ImageData(phAsset);
}
}
At this point I have successfully extracted the URL for the file:-
"/var/mobile/Media/DCIM/100APPLE/IMG_0036.JPG"
However when I run my email function, even though the url is populated, I get a null exception without any explanation that I know of.
// (4.) Email functionality //
https://developer.xamarin.com/recipes/ios/shared_resources/email/send_an_email/
partial void BtnMessageDone_TouchUpInside(UIButton sender)
{
MFMailComposeViewController mailController;
if (MFMailComposeViewController.CanSendMail)
{
StringBuilder htmlBodyMail = FormatEmailBody();
mailController = new MFMailComposeViewController();
// do mail operations here
mailController.SetToRecipients(new string[] { "xxx.yy#email.com" });
mailController.SetSubject("mail test");
mailController.SetMessageBody(htmlBodyMail.ToString(), false);
UIImage img = UIImage.FromFile(PhotoDataClass._file);
mailController.AddAttachmentData(img.AsJPEG(), "image/JPG", "Image.JPG");
mailController.Finished += (object s, MFComposeResultEventArgs args) =>
{
Console.WriteLine(args.Result.ToString());
args.Controller.DismissViewController(true, null);
};
this.PresentViewController(mailController, true, null);
}
}
The null exception occurs on the line inside the email functionality:-
mailController.AddAttachmentData(img.AsJPEG(), "image/JPG", "Image.JPG");
From your code, the image will be null when you use UIImage img = UIImage.FromFile(PhotoDataClass._file);. So this null exception will be thrown.
We should use PHAsset to retrieve image from system photo library instead of using the url directly. You can store the LocalIdentifier to the class PhotoDataClass, then retrieve the PHAsset through this identifier when you want to use it later.
Modify the ImageData method:
void ImageData(PHAsset asset)
{
if (asset == null)
throw new Exception("PHAsset is null");
PhotoDataClass.AssetIdentifier = asset.LocalIdentifier;
}
Then BtnMessageDone_TouchUpInside method can be:
MFMailComposeViewController mailController;
if (MFMailComposeViewController.CanSendMail)
{
var results = PHAsset.FetchAssetsUsingLocalIdentifiers(new string[] { PhotoDataClass.AssetIdentifier }, null);
foreach (PHAsset asset in results)
{
if (asset.LocalIdentifier == PhotoDataClass.AssetIdentifier)
{
PHImageManager.DefaultManager.RequestImageData(asset, null, (data, dataUti, orientation, info) =>
{
StringBuilder htmlBodyMail = FormatEmailBody();
mailController = new MFMailComposeViewController();
// do mail operations here
...
UIImage img = UIImage.LoadFromData(data);
mailController.AddAttachmentData(img.AsJPEG(), "image/JPG", "Image.JPG");
...
});
}
}
}
Moreover That LastOrDefault() is not what I think it is
This is because your NSSortDescriptor, use SortDescriptors = new[] { new NSSortDescriptor("creationDate", false) } or phAsset = PHAsset.FetchAssets(options).FirstOrDefault() as PHAsset; to get your photo you just capture from your camera.
I am building an app, that allows user to take a picture, extract meta data and attach photo to Email + meta data information.
However the issue is that If you capture a photo from camera, the event does not return a ReferenceUrl, atleast I dont see one. I do get some form of URL but that doesn't work as intended. Here is my code:-
// Take a photo with the Camera //
partial void BtnCamera_TouchUpInside(UIButton sender)
{
UIImagePickerController imagePicker = new UIImagePickerController();
imagePicker.PrefersStatusBarHidden();
imagePicker.SourceType = UIImagePickerControllerSourceType.Camera;
// handle saving picture and extracting meta-data from picture //
imagePicker.FinishedPickingMedia += Handle_FinishedPickingMedia;
// present //
PresentViewController(imagePicker, true, () => { });
}
To save the image I call the SaveImageToPhone() from my FinishedPickingMedia handler. It looks like this:-
private static void SaveImagetoPhone(UIImagePickerMediaPickedEventArgs e)
{
NSUrl url = null;
void ImageData(PHAsset asset)
{
if (asset == null)
throw new Exception("PHAsset is null");
//PHImageManager.DefaultManager.RequestImageData(asset, null, (data, dataUti, orientation, info) =>
// {
// var urlX = info.ValueForKey(new NSString("PHImageFileURLKey"));
// url = info.ValueForKey(new NSString("PHImageFileURLKey")) as NSUrl;
// DataClass._fileURL = urlX.ToString();
// });
}
PHAsset phAsset;
if (e.ReferenceUrl == null)
{
e.OriginalImage?.SaveToPhotosAlbum((image, error) =>
{
if (error == null)
{
var options = new PHFetchOptions
{
FetchLimit = 1,
SortDescriptors = new[] { new NSSortDescriptor("creationDate", true) }
};
phAsset = PHAsset.FetchAssets(options).LastOrDefault() as PHAsset;
ImageData(phAsset);
}
});
}
else
{
phAsset = PHAsset.FetchAssets(new[] { e.ReferenceUrl }, null).FirstOrDefault() as PHAsset;
ImageData(phAsset);
}
}
In this function I have a couple of lines commented out, it is here where I was trying to get the reference URL for the Photo (unsuccessfully).
And finally my email () looks like this:-
partial void BtnMessageDone_TouchUpInside(UIButton sender)
{
MFMailComposeViewController mailController;
if (MFMailComposeViewController.CanSendMail)
{
StringBuilder htmlBodyMail = FormatEmailBody();
mailController = new MFMailComposeViewController();
// do mail operations here
mailController.SetToRecipients(new string[] { "xxyyzz#...com" });
mailController.SetSubject("mail test");
mailController.SetMessageBody(htmlBodyMail.ToString(), false);
UIImage img = UIImage.FromFile(DataClass._fileURL);
mailController.AddAttachmentData(img.AsJPEG(), "image/JPG", "Image.JPG");
mailController.Finished += (object s, MFComposeResultEventArgs args) =>
{
Console.WriteLine(args.Result.ToString());
args.Controller.DismissViewController(true, null);
};
this.PresentViewController(mailController, true, null);
}
Does anyone have any pointers on how to get the absolute path/reference URL of the photo taken by the in app camera and attach it to the email body ?
Use the Path property on the NSUrl class:
NSString urlString = new NSString("file:///stack/over%20flow/foobar.txt");
NSUrl myFileUrl = new NSUrl (urlString);
Console.WriteLine (myFileUrl.AbsoluteString);
string absPath = myFileUrl.Path;
Console.WriteLine (absPath);
I am using Xamarin iOS on windows Visual studio to extract metadata from a photo taken by the iphone camera.
private void GetMetaData(NSUrl url)
{
CGImageSource myImageSource;
myImageSource = CGImageSource.FromUrl(url, null);
var ns = new NSDictionary();
var imageProperties = myImageSource.CopyProperties(ns, 0);
var gps = imageProperties.ObjectForKey(CGImageProperties.GPSDictionary) as NSDictionary;
var lat = gps[CGImageProperties.GPSLatitude];
var latref = gps[CGImageProperties.GPSLatitudeRef];
var lon = gps[CGImageProperties.GPSLongitude];
var lonref = gps[CGImageProperties.GPSLongitudeRef];
var loc = String.Format("GPS: {0} {1}, {2} {3}", lat, latref, lon, lonref);
Console.WriteLine(loc);
}
the url being passed into the method is as:- {file:///var/mobile/Media/DCIM/100APPLE/IMG_0006.JPG}
The CGImageSource.FromUrl(url, null) returns null and my app crashes... Can anyone explain to me how I need to fix this?
Edit This is how I am getting the URL for the image.
protected void Handle_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
{
NSUrl url = null;
try
{
void ImageData(PHAsset asset)
{
if (asset == null) throw new Exception("PHAsset is null");
PHImageManager.DefaultManager.RequestImageData(asset, null, (data, dataUti, orientation, info) =>
{
//Console.WriteLine(data);
Console.WriteLine(info);
url = info.ValueForKey(new NSString("PHImageFileURLKey")) as NSUrl;
// Call method to get MetaData from Image Url //
GetMetaData(url);
});
}
As I said in your last post: Xamarin iOS camera and photos: If you capture photos from camera, the event will not return a ReferenceUrl. But you can get the metadata info with another key:
protected void Handle_FinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
{
var metadataInfo = e.Info["UIImagePickerControllerMediaMetadata"]; //or e.MediaMetadata;
}
This will contain some basic information which may be helpful to you. But this NSDictionary doesn't contain the GPS information. Because this picture is created by yourself, you should get the GPS by using CLLocationManager manually:
CLLocationManager manager;
manager = new CLLocationManager();
manager.RequestWhenInUseAuthorization();
manager.LocationsUpdated += (locationSender, e) =>
{
//get current locations
manager.StopUpdatingLocation();
};
manager.StartUpdatingLocation();
Please notice that add the keys in info.plist: NSLocationWhenInUseUsageDescription and NSLocationAlwaysAndWhenInUsageDescription on iOS11, if you want to deploy iOS10 add NSLocationAlwaysUsageDescription.
In this way, there's no need to get metadata from CGImageSource when you pick photos from camera.
I get random memory crashes with no memory warning. I have updated my ios to 10.0.2, my Xamarin to 6.2. I added camera usage description and photo library usage description, as suggested.
I have the following code in my FinishedPickingMedia callback
public async override void FinishedPickingMedia(UIImagePickerController picker, NSDictionary info)
{
try
{
// determine what was selected, video or image
bool isImage = false;
switch (info[UIImagePickerController.MediaType].ToString())
{
case "public.image":
isImage = true;
break;
case "public.video":
break;
}
if (isImage)
{
UIImage originalImage = info[UIImagePickerController.OriginalImage] as UIImage;
if (originalImage != null)
{
UIImageOrientation OrIn = originalImage.Orientation;
Debug.WriteLine("scaling image");
var originalImage1 = await Task.Run(() => ScaleAndRotateImage.ScaleAndRotateImageView(originalImage, OrIn));
if (originalImage1 != null)
{
var Data = originalImage1.AsJPEG(0.0f);
if (Data != null)
{
UIImage resizedImage = UIImage.LoadFromData(Data);
if (originalImage != null)
{
originalImage.Dispose();
originalImage = null;
}
originalImage1.Dispose();
originalImage1 = null;
Data.Dispose();
Data = null;
GC.Collect();
#if DEBUG
var Process1 = Process.GetCurrentProcess();
if (Process1 != null)
{
Debug.WriteLine(string.Format("memory allocated by FinishedPickingMedia = {0}", Process1.WorkingSet64));
}
#endif
camController.ShowPhoto(resizedImage);
}
if (info != null)
{
info.Dispose();
info = null;
}
GC.Collect();
}
}
}
}
catch (Exception ex)
{
Debug.WriteLine("Exception Occured in FinishedPickingMedia method due to " + ex.Message);
}
finally
{
// dismiss the picker
picker.DismissModalViewController(true);
picker.Dispose();
picker = null;
GC.Collect();
}
}
This seams to happen randomly, my application needs to capture more than 200 photos (archive) I researched on the internet and this seams to be an issue with iOS 10 and this control
I´ve done a picture selector via UIImagePickerController. Because of the memory issues this one has I want to save the selected image to disc and if needed load it from filepath. But I can´t manage to get it working.
If i bind the original image directly it is displayed with no problems.
File.Exists in the code returns true but image in the last line is null if watched in debugger.. Thank you very much for your help!
NSData data = originalImage.AsPNG();
string path = Environment.GetFolderPath (Environment.SpecialFolder.Personal);
string pathTempImage = Path.Combine(path, "tempImage.png");
byte[] tempImage = new byte[data.Length];
File.WriteAllBytes(pathTempImage, tempImage);
if(File.Exists(pathTempImage))
{
int i = 0;
}
UIImage image = UIImage.FromFile(pathTempImage);
Update
This is the code that works for me:
void HandleFinishedPickingMedia (object sender, UIImagePickerMediaPickedEventArgs e)
{
_view.DismissModalViewControllerAnimated (true);
BackgroundWorker bw = new BackgroundWorker();
bw.DoWork += delegate(object bwsender, DoWorkEventArgs e2) {
// determine what was selected, video or image
bool isImage = false;
switch(e.Info[UIImagePickerController.MediaType].ToString()) {
case "public.image":
Console.WriteLine("Image selected");
isImage = true;
break;
case "public.video":
Console.WriteLine("Video selected");
break;
}
// get common info (shared between images and video)
NSUrl referenceURL = e.Info[new NSString("UIImagePickerControllerReferenceUrl")] as NSUrl;
if (referenceURL != null)
Console.WriteLine("Url:"+referenceURL.ToString ());
// if it was an image, get the other image info
if(isImage) {
// get the original image
originalImage = e.Info[UIImagePickerController.OriginalImage] as UIImage;
if(originalImage != null) {
NSData data = originalImage.AsPNG();
_picture = new byte[data.Length];
ImageResizer resizer = new ImageResizer(originalImage);
resizer.RatioResize(200,200);
string path = Environment.GetFolderPath (Environment.SpecialFolder.Personal);
string pathTempImage = Path.Combine(path, "tempImage.png");
string filePath = Path.Combine(path, "OriginalImage.png");
NSData dataTempImage = resizer.ModifiedImage.AsPNG();
byte[] tempImage = new byte[dataTempImage.Length];
System.Runtime.InteropServices.Marshal.Copy(dataTempImage.Bytes,tempImage,0,Convert.ToInt32(tempImage.Length));
//OriginalImage
File.WriteAllBytes(filePath, _picture);
//TempImag
File.WriteAllBytes(pathTempImage, tempImage);
UIImage image = UIImage.FromFile(pathTempImage);
_view.InvokeOnMainThread (delegate {
templateCell.BindDataToCell(appSelectPicture.Label, image);
});
_picture = null;
}
} else { // if it's a video
// get video url
NSUrl mediaURL = e.Info[UIImagePickerController.MediaURL] as NSUrl;
if(mediaURL != null) {
Console.WriteLine(mediaURL.ToString());
}
}
// dismiss the picker
};
bw.RunWorkerAsync();
bw.RunWorkerCompleted += HandleRunWorkerCompleted;
}
byte[] tempImage = new byte[data.Length];
File.WriteAllBytes(pathTempImage, tempImage);
You're not copying the image data to your allocated array before saving it. That result in a large empty file that is not a valid image.
Try using one of the NSData.Save overloads, like:
NSError error;
data.Save (pathTempImage, NSDataWritingOptions.FileProtectionNone, out error);
That will allow you to avoid allocating the byte[] array.