tl;dr: the layer.attachments.get_list() returns 'none' for EXIF metadata for a given attachment with the metadata is definitely there.
The same record, pulling the latitude via an Arcade form calculation:
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.
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.
@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.
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.