0

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.

gaelian
  • 105
  • 10
  • Most times I see webpages (html) with the image as a link and the gps data as tags in the page. – jdweng Apr 25 '20 at 07:44
  • If you write the GPS data to the console, are you able to see the information? – Saamer Apr 26 '20 at 13:17
  • Are you not able to store any meta data or just the GPS? You could reference this too https://stackoverflow.com/questions/35758510/save-the-exif-metadata-using-the-new-phphotolibrary – Saamer Apr 28 '20 at 06:51
  • At the point when the GPS data is being saved to `destinationOptions.GpsDictionary` (i.e. `destinationOptions.GpsDictionary = new CGImagePropertiesGps(meta[ImageIO.CGImageProperties.GPSDictionary] as NSDictionary);`, the expected data is there – gaelian Apr 29 '20 at 05:06
  • Just the GPS data. Other metadata is present. I have seen the link you reference. At this point I'm not even trying to save the photo to the gallery, just to the app folder, via the use of `CGImageDestination` etc. – gaelian Apr 29 '20 at 07:10
  • @gaelian did you resolve this? – Marco Duindam Mar 01 '22 at 18:13
  • @MarcoDuindam you may find this useful: https://github.com/dimonovdd/Xamarin.MediaGallery – gaelian Mar 08 '22 at 08:17

0 Answers0