Select to view content in your preferred language

layer.attachments.get_list() not returning EXIF data

230
3
03-27-2025 01:50 PM
SFM_TravisBott
Frequent Contributor

tl;dr: the layer.attachments.get_list() returns 'none' for EXIF metadata for a given attachment with the metadata is definitely there. 

SFM_TravisBott_0-1743108432447.png

The same record, pulling the latitude via an Arcade form calculation:

SFM_TravisBott_1-1743108503646.png

What gives?

Premise: I have a feature service with photo points, whose orientation I'd like to be reflected in our maps. As such, there are fields to store orientation angle, as well as latitude and longitude in the photo. These points are captured both in Survey123 and Field Maps, depending on the situation. Pulling EXIF data is simple in S123 and relatively simply when editing in a form in a web app. However, capturing the EXIF data for photos collected in Field Maps has been challenging. 

Form calculations in Field Maps do not work, as the photo is not actually part of the service yet. As such, my idea is to run a script and update them post-facto. However, the attachments call seems to ignore metadata, which makes the workaround not so good. 

0 Kudos
3 Replies
DavidSolari
MVP Regular Contributor

My understanding is the "ExifInfo" field is only used by Survey123 to make grabbing EXIF data faster. You'll have to do this the old-fashioned way: call download, then open the file with a library that can read the EXIF data. I'm pretty sure Pillow is a standard module with Pro so you can do something like this to get the data.

SFM_TravisBott
Frequent Contributor

@DavidSolari Thanks for the comment. This is another unfortunate episode of Esri's platform being so disjointed...somethings work great in some workflows but are darn near impossible in others.

As per this workaround, it's certainly a workaround but good lord is that cumbersome. I've done some tooling around with Pillow and have managed to get what I need out of a given image....but the idea of needing to pull down all of the images that need their data updated is such a pain. I'd rather not be in the business of needing to download hundreds of images just to get data that's immediately extractable if using Survey123. 

0 Kudos
SFM_TravisBott
Frequent Contributor

Replying to my own post in case this helps anyone...I at least found a method to do so in Python after the fact. Forgive me for sloppy code...

import requests
import arcgis
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS, IFD
from io import BytesIO
import arcpy

userName = ''

print("Log in to Org Account")
gis = arcgis.GIS("https://yourOrg.maps.arcgis.com", username= userName)
print("Successfully logged in as " + str(gis.properties.user.username)

def get_exif_data(self):
    exif_data = {}
    info = self._getexif()

    if info:
        for tag, value in info.items():
            decoded = TAGS.get(tag, tag)
            if decoded == "GPSInfo":
                gps_data = {}
                for t in value:
                    sub_decoded = GPSTAGS.get(t, t)
                    gps_data[sub_decoded] = value[t]

                exif_data[decoded] = gps_data
            else:
                exif_data[decoded] = value

    return exif_data

def convert_to_degress(value):
    """Helper function to convert the GPS coordinates
    stored in the EXIF to degress in float format"""
    d = float(value[0])
    m = float(value[1])
    s = float(value[2])

    return d + (m / 60.0) + (s / 3600.0)

def get_lat(self):
    if 'GPSInfo' in self:
        gps_info = self['GPSInfo']
        if 'GPSLatitude' and 'GPSLatitudeRef' in gps_info:
            gps_lat = gps_info['GPSLatitude']
            gps_lat_ref = gps_info['GPSLatitudeRef']
            if gps_lat and gps_lat_ref:
                lat = convert_to_degress(gps_lat)
                if gps_lat_ref !='N':
                    lat = 0-lat
                print(f'GPSLatitude: {lat}')
                return lat
        else:
            print('GPSLatitude not found!')
            return None
    
    else:
        return None

def get_long(self):
    if 'GPSInfo' in self:
        gps_info = self['GPSInfo']
        if 'GPSLongitude' and 'GPSLongitudeRef' in gps_info:
            gps_long = gps_info['GPSLongitude']
            gps_long_ref = gps_info['GPSLongitudeRef']
            if gps_long and gps_long_ref:
                long = convert_to_degress(gps_long)
                if gps_long_ref !='E':
                    long = 0-long
                print(f'GPSLongitude: {long}')
                return long
        else:
            print("GPSLongitude not found!")
            return None
    else:
        return None

def get_angle(self):
    if 'GPSInfo' in self:
        gps_info = self['GPSInfo']
        if 'GPSImgDirection' in gps_info:
            gps_angle = gps_info['GPSImgDirection']

            if gps_angle:
                print(f'GPSImgDirection: {gps_angle}')
                return gps_angle
        else:
            print(f'GPSImgDirection not found!')
            return None
    else:
        return None

layerID = ''
layerItem = arcgis.gis.Item(gis, layerID)
photoPointLayer = layerItem.layers[]

where_clause = "PhotoAngle IS NULL OR PhotoLat IS NULL OR PhotoLong IS NULL"

photoPointFSet = photoPointLayer.query(where=where_clause, return_geometry=False)

runCount = len(photoPointFSet)

if runCount > 0:
    print(f"---{runCount} features returned that require EXIF metadata updates---")

    fieldsList = ['OBJECTID', 'PhotoAngle', 'PhotoLat', 'PhotoLong']

    with arcpy.da.UpdateCursor(photoPointLayer.url, fieldsList, where_clause) as cursor:

        for row in cursor:

            OID = row[0]
            attachmentInfo = photoPointLayer.attachments.search(object_ids=[OID])

            for attachment in attachmentInfo:

                downloadURL = attachment["DOWNLOAD_URL"]

                response = requests.get(downloadURL, stream=True)
                if response.status_code == 200:
                    img = Image.open(BytesIO(response.content))
                    exif_data = get_exif_data(img)
                    
                    if exif_data:
                        # exif = {TAGS.get(tag, tag): value for tag, value in exif_data.items()}
                        print(f"EXIF metadata for {attachment['NAME']}:\n", exif_data)
                        exifAngle = get_angle(exif_data)
                        exifLat = get_lat(exif_data)
                        exifLong = get_long(exif_data)
                        
                        if exifAngle or exifLat or exifLong:
                            if exifAngle:
                                row[1] = exifAngle
                            if exifLat:
                                row[2] = exifLat
                            if exifLong:
                                row[3] = exifLong
                            cursor.updateRow(row)
                    else:
                        print(f"No EXIF metadata found for {attachment['NAME']}")
                else:
                    print(f"Failed to access {attachment['NAME']}")
else:
    print('No features require updating. Have a nice day.')

 

This is also apparently possible with a Power Automate connector but haven't tried it yet.

 

0 Kudos