I have a file geodatabase with survey data regarding waterbodies. Each point/waterbody has 2 to 4 photo attachments that correspond to the specific point.

I found a script online that will connect the Rel_GlobalID from the attachment table to the Feature Layer Table. However, the script below only saves a single photograph from the point, and does not add other photos.The format for file names should be something like:


from arcpy import da
import os

attachTable = arcpy.GetParameterAsText(0) # table in GDB holding attachments
origTable = arcpy.GetParameterAsText(1) # layer in GDB holding features to which attachments belong
nameField = arcpy.GetParameterAsText(2) # field in origTable that contains more appropriate name for attachment
fileLocation = arcpy.GetParameterAsText(3) # folder where you want to save the photos

# create the cursor to search through the attachment tables; specify you only wish to search those three fields
attachCursor = da.SearchCursor(attachTable,['DATA','ATT_NAME','GLOBALID','REL_GLOBALID'])

# begin searching the table and storing the actual images, movies, etc
for attRow in attachCursor:
    binaryRep = attRow[0]
    fileName = attRow[1]
    relID = attRow[2] # defines the relationship ID that we can use to join to the actual features (origTable)

    # creates a cursor to sort the features; essentially to find a match for the relID above
    originCursor = da.SearchCursor(origTable,['GlobalID', nameField])
    for origRow in originCursor:
        origID = origRow[0] # store the Global ID (which will match to the relID above for the correct attachment)
        origName = origRow[1] # store the unique name of each record that you will use to save the attachment
        if origID == relID: # stops the search after it finds the record which equals the ID from the attachments

    # saves a file in the specified location that contains the name chosen by the user for that attachment    
    open(fileLocation + os.sep + origName + ".jpg",'wb').write(binaryRep.tobytes())

    # iteratively deletes the searchCursor from the feature class so it can reboot for the next attachment!
    del originCursor

# If you are creating script from Sratch in ArcGIS
# Parameter(0) Type = Table
# Parameter(1) Type = Feature Layer
# Parameter(2) Type = Field (obtained from Parameter(1)
# Parameter(3) Type = Folder (make sure it is an input) ```

3 Answers 3


See below, a similar script I have used in the past. You can turn it into an ArcTool with parameters. The basic workflow is, get the information you need for the file naming from the feature class attributes and store in a dictionary where the key is the GlobalID and the value is the attribute(s) - use a list as the value if you need multiple attributes to make up your filename. Then iterate over the attachment table exporting each attachment and renaming using the dictionary based on the REL_GLOBALID. You might need to rename some of the hard coded field names so that they match what you see in your table/fc. This also assumes that your attachments are indeed named uniquely per ID, if not you will have to work around that.

import arcpy

## the fc that contains the IDs/info
input_fc = r"path/to/fc"
## the attachment table to extract from
input_tbl = r"path/to/attachment/tbl"
## the folder to send the extracted attachments to
output_fldr = r"path/to/output/folder"

## populate dictionary with key: GlobalID, value: the Waterbody_ID (fieldName)
survey_dict = {}

with arcpy.da.SearchCursor(input_fc, ["GlobalID", nameField]) as cursor:
    for row in cursor:
        survey_dict[row[0]] = row[1]

## iterate through the table
with arcpy.da.SearchCursor(input_tbl, ['DATA', 'ATT_NAME', 'REL_GLOBALID']) as cursor:
    ## for each row
    for item in cursor:
        ## create the attachments name
        ## use the survey_dict to find the matching GlobalID to REL_GLOBALID
        attach_name = "{0}_{1}".format(survey_dict(item[2]), item[1])
        ## write to the output folder
        open("{0}\\{1}".format(output_fldr, attach_name), 'wb').write(item[0].tobytes())

Change your script as follows:

# creates a cursor to sort the features; essentially to find a match for the relID above
sql = f"GlobalID = {relID}"
with da.SearchCursor(origTable,['GlobalID', nameField], where_clause=sql) as cursor:
  for _, name in cursor:
    with open(os.path.join(fileLocation, f"{name}.jpg"), "wb") as fp:

This does, per line:

  1. Set the SQL where clause so your cursor only returns the records you care about.
  2. Open the cursor in a Python context (this will automatically dispose of it when it's done) using the where clause.
  3. Loop through attachments by name.
  4. Open a file pointer using a Python context (the other code samples in this problem seem to conveniently ignore the fact that this is a resource in addition to the cursor).
  5. Write the file to the file pointer.

The thing that was stopping you was the break statement, it stops processing the cursor loop when it hits the first attachment. Likely, the original author's problem only contained one attachment so that was enough for them.

You do not need the del originCursor statement. arcpy cursors behave very will using with context.

The sql variable as set is slightly dangerous, liable to a SQL injection attack if someone gets funky stuff into your attachment table. If you're using this alone and your schema is constrained to numeric IDs, you should be fine.


Clubdebambos's script worked great, with one minor error: Replace the parenthesis with brackets for survey_dict on the attach_name file:

attach_name = "{0}_{1}".format(survey_dict[item[2]], item[1])

I took the time to turn the script into a parameterized ArcGIS Pro tool and account for ObjectID vs GlobalID input. Here is the script to do so:

import arcpy

## Script tool for use in ArcGIS Pro to export attachments (often photos) from a table related to a feature class.
## Usually captured with an Esri field app like Survey123 or Field Maps.

## Script derived from gis.stackexchange user Clubdebambos. Edited by Tim Ultee and Chris O'Neal.

## the fc that contains the IDs/info
input_fc = arcpy.GetParameterAsText(0)
## the attachment table to extract from
input_tbl = arcpy.GetParameterAsText(1)
## the folder to send the extracted attachments to
output_fldr = arcpy.GetParameterAsText(2)

## field name in the feature class containing desired exported file name
fieldName = arcpy.GetParameterAsText(3)

## variable for file name prefix
filePrefix = arcpy.GetParameterAsText(4)

## variable for determining if inputs use GlobalIDs or ObjectIDs
global_object_id = arcpy.GetParameterAsText(5)
if global_object_id == "true":
    featureId = "GLOBALID"
    relId = "REL_GLOBALID"
    featureId = "OBJECTID"
    relId = "REL_OBJECTID"

## populate dictionary with key: GlobalID or ObjectID, value: the fieldName (ex: Tree_ID, Manhole_ID)
survey_dict = {}

with arcpy.da.SearchCursor(input_fc, [featureId, fieldName]) as cursor:
    for row in cursor:
        survey_dict[row[0]] = row[1]

## iterate through the table
with arcpy.da.SearchCursor(input_tbl, ['DATA', 'ATT_NAME', relId]) as cursor:
    ## for each row
    for item in cursor:
        if item[2] in survey_dict:
            attach_name = filePrefix + "{0}_{1}".format(survey_dict[item[2]], item[1])
            #create the attachments name
            #use the survey_dict to find the matching GlobalID/ObjectID to REL_GLOBALID/REL_OBJECTID
            open("{0}\\{1}".format(output_fldr, attach_name), 'wb').write(item[0].tobytes())
            print("No mapping found for ", relId, ": ", item[2])

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.