It would appear that something has changed in recent versions of iOS as it relates to saving GPS data in photos and subsequent to a bug in the Xam.Plugin.Media library, I am attempting to figure out how to take a photo and save it to the Photo Gallery with intact GPS data using iOS 13.4.1 and Xamarin.iOS 13.16.0.13. I have looked at the internals of Xam.Plugin.Media and using this as a starting point, tried to cobble something together on the hunch that because Xam.Plugin.Media uses ALAssetsLibrary to save to the Photo Gallery, perhaps this was why the GPS data is not being saved with the photo.
I am currently at the point where I had thought I would be able to take the photo, merge GPS data into the file and save the JPG output to a temporary location in the app folder (i.e. I haven't even gotten to saving the photo to the gallery yet). But when I examine this temp file using either the Preview app on MacOS or Photoshop to view the GPS metadata, the data is not there.
My handler for UIImagePickerController.FinishedPickingMedia
is this:
private void OnFinishedPickingMedia(object sender, UIImagePickerMediaPickedEventArgs e)
{
try
{
NSDictionary info = e.Info;
NSDictionary meta = null;
UIImage image = null;
string savedImagePath = null;
if (_imagePicker.SourceType == UIImagePickerControllerSourceType.Camera)
{
meta = (NSDictionary)info[UIImagePickerController.MediaMetadata];
image = (UIImage)info[UIImagePickerController.OriginalImage];
if (meta != null && meta.ContainsKey(ImageIO.CGImageProperties.Orientation))
{
var newMeta = new NSMutableDictionary();
newMeta.SetValuesForKeysWithDictionary(meta);
var newTiffDict = new NSMutableDictionary();
newTiffDict.SetValuesForKeysWithDictionary(meta[ImageIO.CGImageProperties.TIFFDictionary] as NSDictionary);
newTiffDict.SetValueForKey(meta[ImageIO.CGImageProperties.Orientation], ImageIO.CGImageProperties.TIFFOrientation);
newMeta[ImageIO.CGImageProperties.TIFFDictionary] = newTiffDict;
meta = newMeta;
}
if (_locationPermissionGranted)
{
meta = SetGpsLocation(meta);
}
savedImagePath = SaveImageWithMetadata(image, meta);
}
string aPath = null;
if (_imagePicker.SourceType != UIImagePickerControllerSourceType.Camera)
{
// Try to get the album path's URL.
var url = (NSUrl)info[UIImagePickerController.ReferenceUrl];
aPath = url?.AbsoluteString;
}
}
catch (Exception ex)
{
Console.WriteLine($"Unable to get metadata: {ex}");
}
}
Which calls:
private NSDictionary SetGpsLocation(NSDictionary meta)
{
var newMeta = new NSMutableDictionary();
newMeta.SetValuesForKeysWithDictionary(meta);
var newGpsDict = new NSMutableDictionary();
newGpsDict.SetValueForKey(new NSNumber(Math.Abs(_lat)), ImageIO.CGImageProperties.GPSLatitude);
newGpsDict.SetValueForKey(new NSString(_lat > 0 ? "N" : "S"), ImageIO.CGImageProperties.GPSLatitudeRef);
newGpsDict.SetValueForKey(new NSNumber(Math.Abs(_long)), ImageIO.CGImageProperties.GPSLongitude);
newGpsDict.SetValueForKey(new NSString(_long > 0 ? "E" : "W"), ImageIO.CGImageProperties.GPSLongitudeRef);
newGpsDict.SetValueForKey(new NSNumber(_altitude), ImageIO.CGImageProperties.GPSAltitude);
newGpsDict.SetValueForKey(new NSNumber(0), ImageIO.CGImageProperties.GPSAltitudeRef);
newGpsDict.SetValueForKey(new NSNumber(_speed), ImageIO.CGImageProperties.GPSSpeed);
newGpsDict.SetValueForKey(new NSString("K"), ImageIO.CGImageProperties.GPSSpeedRef);
newGpsDict.SetValueForKey(new NSNumber(_direction), ImageIO.CGImageProperties.GPSImgDirection);
newGpsDict.SetValueForKey(new NSString("T"), ImageIO.CGImageProperties.GPSImgDirectionRef);
newGpsDict.SetValueForKey(new NSString(_timestamp.ToString("hh:mm:ss")), ImageIO.CGImageProperties.GPSTimeStamp);
newGpsDict.SetValueForKey(new NSString(_timestamp.ToString("yyyy:MM:dd")), ImageIO.CGImageProperties.GPSDateStamp);
newMeta[ImageIO.CGImageProperties.GPSDictionary] = newGpsDict;
return newMeta;
}
private string SaveImageWithMetadata(UIImage image, NSDictionary meta)
{
string outputPath = null;
try
{
var finalQuality = 1.0f;
var imageData = image.AsJPEG(finalQuality);
// Continue to move down quality, rare instances.
while (imageData == null && finalQuality > 0)
{
finalQuality -= 0.05f;
imageData = image.AsJPEG(finalQuality);
}
if (imageData == null)
{
throw new NullReferenceException("Unable to convert image to jpeg, please ensure file exists or " +
"lower quality level");
}
var dataProvider = new CGDataProvider(imageData);
var cgImageFromJpeg = CGImage.FromJPEG(dataProvider, null, false, CGColorRenderingIntent.Default);
var imageWithExif = new NSMutableData();
var destination = CGImageDestination.Create(imageWithExif, UTType.JPEG, 1);
var cgImageMetadata = new CGMutableImageMetadata();
var destinationOptions = new CGImageDestinationOptions();
if (meta.ContainsKey(ImageIO.CGImageProperties.Orientation))
{
destinationOptions.Dictionary[ImageIO.CGImageProperties.Orientation] =
meta[ImageIO.CGImageProperties.Orientation];
}
if (meta.ContainsKey(ImageIO.CGImageProperties.DPIWidth))
{
destinationOptions.Dictionary[ImageIO.CGImageProperties.DPIWidth] =
meta[ImageIO.CGImageProperties.DPIWidth];
}
if (meta.ContainsKey(ImageIO.CGImageProperties.DPIHeight))
{
destinationOptions.Dictionary[ImageIO.CGImageProperties.DPIHeight] =
meta[ImageIO.CGImageProperties.DPIHeight];
}
if (meta.ContainsKey(ImageIO.CGImageProperties.ExifDictionary))
{
destinationOptions.ExifDictionary =
new CGImagePropertiesExif(meta[ImageIO.CGImageProperties.ExifDictionary] as NSDictionary);
}
if (meta.ContainsKey(ImageIO.CGImageProperties.TIFFDictionary))
{
destinationOptions.TiffDictionary =
new CGImagePropertiesTiff(meta[ImageIO.CGImageProperties.TIFFDictionary] as NSDictionary);
}
if (meta.ContainsKey(ImageIO.CGImageProperties.GPSDictionary))
{
destinationOptions.GpsDictionary =
new CGImagePropertiesGps(meta[ImageIO.CGImageProperties.GPSDictionary] as NSDictionary);
}
if (meta.ContainsKey(ImageIO.CGImageProperties.JFIFDictionary))
{
destinationOptions.JfifDictionary =
new CGImagePropertiesJfif(meta[ImageIO.CGImageProperties.JFIFDictionary] as NSDictionary);
}
if (meta.ContainsKey(ImageIO.CGImageProperties.IPTCDictionary))
{
destinationOptions.IptcDictionary =
new CGImagePropertiesIptc(meta[ImageIO.CGImageProperties.IPTCDictionary] as NSDictionary);
}
destination.AddImageAndMetadata(cgImageFromJpeg, cgImageMetadata, destinationOptions);
var success = destination.Close();
if (success)
{
outputPath = GetOutputPath();
imageWithExif.Save(outputPath, true);
}
}
catch (Exception e)
{
Console.WriteLine($"Unable to save image with metadata: {e}");
}
return outputPath;
}
private static string GetOutputPath()
{
var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "temp");
Directory.CreateDirectory(path);
var timestamp = DateTime.UtcNow.ToString("yyyMMdd_HHmmss", CultureInfo.InvariantCulture);
var name = "IMG_" + timestamp + ".jpg";
return Path.Combine(path, GetUniquePath(path, name));
}
private static string GetUniquePath(string path, string name)
{
var ext = Path.GetExtension(name);
name = Path.GetFileNameWithoutExtension(name);
var fullName = name + ext;
var i = 1;
while (File.Exists(Path.Combine(path, fullName)))
{
fullName = name + "_" + (i++) + ext;
}
return Path.Combine(path, fullName);
}
The file is saved successfully, but without the expected GPS metadata. Which leads me to believe that the problem may be to do with the way I am saving the temporary photo or saving the GPS metadata to it in either SaveImageWithMetadata
or SetGpsLocation
.
If anyone can provide some info on what actually works now with iOS 13.4.1 and saving GPS data to photos, I would be very appreciative.