developers

How to Read and Remove Metadata from Your Photos With Python

Smartphones include EXIF metadata in their photos. Here’s how to read, write, and erase it using Python.

Feb 15, 20211 min read

There’s Metadata in Your Photos

When you take a picture, you record more than just an image. Smartphones have given us two significant consequences whose effects on privacy and security aren’t yet fully understood:

  1. More people than ever have a camera that’s usually within arm’s reach.
  2. By default, the photos taken with these cameras include sensitive information, and many users are unaware it’s happening.

Along with the pixel data that make up the picture, smartphones and digital cameras — essentially portable, sensor-rich, location-aware, always-networked computers — embed additional data within the picture file. This data can include:

  • The make and model of the device used to take the photo
  • The dimensions and pixel density of the photo
  • Zoom, aperture, flash, and other camera settings
  • When and where the photo was taken
  • The orientation of the device
  • Which direction the camera was facing
  • The altitude
  • The speed at which the photographer was moving
  • A text description of the image
  • Copyright information

EXIF metadata, privacy, and security

Photo metadata is in EXIF format, which is short for EXchangeable Image File format, a continually evolving standard for information added to digital images and sound recordings. It’s useful for sorting, cataloging, and searching through photos, which is why it exists. However, it also introduces privacy and security concerns many users don’t consider.

One of the earliest cautionary tales about photo metadata is the 2012 privacy incident involving antivirus company founder John McAfee. It was an exciting time in McAfee’s life, as he was evading law enforcement in South America. He wanted to share this excitement, so he gave exclusive interviews to journalists, which might have been a bad idea in retrospect. One journalist lucky and resourceful enough to get an interview decided to show off his good fortune by posting a photo of McAfee without removing its EXIF metadata, which gave away their location and led to McAfee’s arrest.

In 2016, two students from Harvard University used the location data from photos posted on the dark web to identify 229 drug dealers. The dealers posted pictures of their products online to prove they had what their customers wanted, believing that being on the dark web would protect their anonymity.

The rioters at the U.S. Capitol building on January 6, 2021, gave away their locations when they posted videos of their participation to a social media site that didn’t remove the metadata.

And finally, there’s the issue of combining photo metadata with information from other sources. When combined with web scraping, open source intelligence (OSINT), reverse image search, and AI-based face and object recognition, it’s possible to build a dossier on someone, complete with information such as birthdate, names of family members, work and education history, social media account, interests, and places where they regularly go.

Being the security-conscious developers that I suspect you are, you’re probably asking yourself questions like these:

  • How can I programmatically detect and read the metadata from a photo?
  • How can I alter, add, or erase photo metadata programmatically?

If you have Python 3.8 or later installed on your computer, you can find out through the hands-on exercises below. They’ll cover some Python packages you can incorporate into your applications to extract, add, alter, or erase photo metadata. You’ll use your programming skills and do some detective work along the way!

Project Setup

You can follow along by downloading our sample Jupyter notebook from GitHub, or you can create your own project and install the following packages:

python3 -m pip install --upgrade jupyter
python3 -m pip install --upgrade exif
python3 -m pip install --upgrade reverse_geocoder
python3 -m pip install --upgrade pycountry

These commands install the following modules:

  • jupyter For managing and running our code effectively.
  • exif For reading and modifying EXIF metadata. Most of the code in this article will use this module.
  • reverse_geocoder: A fast offline utility that takes a given latitude/longitude pair and returns the nearest town or city.
  • pycountry: A country lookup utility that you’ll use to convert country codes into their corresponding names.

Downloading the images

All the code examples will assume the required images are in a directory named

images
located in the same directory as your Jupyter Notebook file.

To do the exercises in this article, download the photos from this directory. You will not get the expected results if you use any other images!

Loading Photos and Checking Them for EXIF Data

Let’s put the exif module to work. Consider these two photos, named

palm-tree-1.jpg
and
palm-tree-2.jpg
:

First photo featuring a palm tree beside a lake

Second photo featuring a palm tree beside a lake

Suppose you are asked these questions:

  • Were these photos taken on the same device or two different devices?
  • Which photo was taken first?
  • Where were these photos taken?

To answer these questions, we’ll load the data from these photos’ files into exif

Image
objects and then use those objects to examine the EXIF metadata.

Do the photos contain EXIF metadata?

Enter the following into a new code cell and run it:

from exif import Image

with open("images/palm-tree-1.jpg", "rb") as palm_1_file:
    palm_1_image = Image(palm_1_file)
    
with open("images/palm-tree-2.jpg", "rb") as palm_2_file:
    palm_2_image = Image(palm_2_file)
    
images = [palm_1_image, palm_2_image]

The code opens each file as read-only. It reads the file’s data in binary format into a file object, which it then uses to instantiate an exif

Image
object. Finally, it puts the
Image
objects into an array so that we can iterate over them, performing the same operations on each photo’s EXIF metadata.

Let’s perform our first operation on each photo, confirming that they contain EXIF data. We’ll do this by checking each

Image
object’s
has_exif
property. For every image that contains EXIF data, we’ll use
Image
’s
exif_version
property to display the version of EXIF it’s using.

Enter this into a new code cell and run it:

for index, image in enumerate(images):
    if image.has_exif:
        status = f"contains EXIF (version {image.exif_version}) information."
    else:
        status = "does not contain any EXIF information."
    print(f"Image {index} {status}")

You should see the following results:

Image 0 contains EXIF (version 0232) information.
Image 1 contains EXIF (version 0232) information.

You now know that both photos contain EXIF information and use the same version of EXIF. You’ll need to do more work to determine if they were taken with the same camera or different ones.

What EXIF metadata is available in each photo?

Think of EXIF as a key-value data structure similar to a Python dictionary. EXIF key-value pairs are called tags; each tag can contain a string or numeric value. There are dozens of tags in the current EXIF standard (version 3,0, released in May 2023), and anyone — from smartphone and camera manufacturers to photographers — can add their own.

Here’s a comprehensive list of EXIF tags that you’re likely to find in digital photos. It includes tags that aren’t in the EXIF standard but are provided by a number of devices or software.

EXIF is one of those informal formats where manufacturers pick and choose which features they’ll implement. This means that photos produced by different cameras and smartphones will have different sets of EXIF tags.

You can generally count on smartphones embedding a number of often-used EXIF tags in the photos they take, including the make and model of the smartphone, the date and time when the photo was taken, the location where the phone was taken, and common camera settings.

Given the differences between devices and the availability of tools for editing EXIF data, one of the first things you should do when working with a photo’s EXIF data is to see which tags are available.

The

Image
objects provided by the exif module expose EXIF tags as properties of that object. This means that you can use Python’s built-in
dir()
function on an
Image
object to see which EXIF tags it has.

Run the code below in a new code cell:

image_members = []

for image in images:
    image_members.append(dir(image))

for index, image_member_list in enumerate(image_members):
    print(f"Image {index} contains {len(image_member_list)} members:")
    print(f"{image_member_list}\n")

You’ll see the following output:

Image 0 contains 74 members:
['<unknown EXIF tag 316>', '<unknown EXIF tag 322>', '<unknown EXIF tag 323>', '<unknown EXIF tag 42080>', '_exif_ifd_pointer', '_gps_ifd_pointer', '_segments', 'aperture_value', 'brightness_value', ... ]

Image 1 contains 73 members:
['<unknown EXIF tag 42080>', '_exif_ifd_pointer', '_gps_ifd_pointer', '_segments', 'aperture_value', 'brightness_value', ... ]

As you can see, while both

Image
objects have a lot of common members, the first image contains one more EXIF tag than the second. This means the two photos may be from different devices.

You can use Python sets to determine the members that the images have in common. Run the following in a new code cell:

common_members = set(image_members[0]).intersection(set(image_members[1]))
print(f"Image 0 and Image 1 have {len(common_members)} members in common:")
common_members_sorted = sorted(list(common_members))
print(f"{common_members_sorted}")

You should see this output:

Image 0 and Image 1 have 63 members in common:
['<unknown EXIF tag 42080>', '_exif_ifd_pointer', '_gps_ifd_pointer', '_segments', 'aperture_value', 'brightness_value', 'color_space', ... ]

If you looked at the names of EXIF tags in the documentation (either the list of standard EXIF tags or the extended list), you may have noticed that EXIF tag names are in PascalCase while the EXIF properties in exif

Image
objects are in snakecase. This is because the authors of the exif_ module are striving to follow the Python Style Guide and designed
Image
to convert EXIF tag names into Pythonic property names.

Device Information

Let’s answer the first question about these photos. Were they taken on the same device or two different devices? We already have reason to believe that the answer is “no”.

Getting the make and model

Both images’ EXIF data have a

make
and
model
tag, so let’s use those to determine what kind of devices were used to take these pictures.

Run the following in a new code cell:

for index, image in enumerate(images):
    print(f"Device information - Image {index}")
    print("----------------------------")
    print(f"Make: {image.make}")
    print(f"Model: {image.model}\n")

Here’s the output:

Device information - Image 0
----------------------------
Make: Apple
Model: iPhone 14 Pro

Device information - Image 1
----------------------------
Make: Google
Model: Pixel 7

This confirms that the photos were taken with different devices, and now you know which ones.

Getting additional and optional information about the devices

Let’s gather some more information about the devices used to take the photos, namely the details about their lenses and operating system versions.

Not all devices report the type of lens in their EXIF metadata, so we’ll use

Image
’s
get()
method, which is similar to the
get()
method used by Python’s dictionaries. Like Python dictionaries’
get()
method, the
get()
method provided by exif’s
Image
object gracefully handles the case where the given key does not exist.

Enter the code below into a new code cell and run it. It uses

get()
to attempt to access the lens and operating system versions used in taking the photos. If a particular property doesn’t exist, its value will be displayed as
Unknown
.

for index, image in enumerate(images):
    print(f"Lens and OS - Image {index}")
    print("---------------------")
    print(f"Lens make: {image.get('lens_make', 'Unknown')}")
    print(f"Lens model: {image.get('lens_model', 'Unknown')}")
    print(f"Lens specification: {image.get('lens_specification', 'Unknown')}")
    print(f"OS version: {image.get('software', 'Unknown')}\n")

Here’s the output:

Lens and OS - Image 0
---------------------
Lens make: Apple
Lens model: iPhone 14 Pro back triple camera 6.86mm f/1.78
Lens specification: (2.220000028611935, 9.0, 1.7799999713880652, 2.8)
OS version: 17.1.2

Lens and OS - Image 1
---------------------
Lens make: Google
Lens model: Pixel 7 back camera 6.81mm f/1.85
Lens specification: Unknown
OS version: HDR+ 1.0.585804401zd

Note that the phone used to take image 1 (the Google Pixel 7), doesn’t provide the

lens_specification
property. If we had tried to access it directly (for example, image.lens_make), the result would have been an error. The
get()
method allowed us to provide an alternate value for those non-existent properties.

When Were the Photos Taken?

The next question is: which photo was taken first?

We can find out by looking at each photo’s

datetime_original
property, which specifies the date and time when the photo was taken. The processor in a smartphone is fast enough to record the exact fraction of a second when it takes a photo, and that fraction is stored in the
subsec_time_original
property.

Some phones also record an

offset_time
property, which we can use to determine
datetime
’s offset from UTC.

The following code displays the date and time when each of the photos was taken. If the photo includes time offset data, the code will also display that value.

Run the following in a new code cell:

for index, image in enumerate(images):
    print(f"Date/time taken - Image {index}")
    print("-------------------------")
    print(f"{image.datetime_original}.{image.subsec_time_original} {image.get('offset_time', '')}\n")

You’ll see this output:

Date/time taken - Image 0
-------------------------
2024:01:04 17:31:26.122 -05:00

Date/time taken - Image 1
-------------------------
2024:01:04 17:31:54.326 -05:00

As you can see, image 0 was taken first, and image 1 was taken 28 seconds later.

Where Were the Photos Taken?

Digital cameras have been recording the date and time into their photos from the beginning, but it wasn’t until the smartphone that recording the location where the photo was taken became commonplace. In this section, we’ll look at accessing the GPS coordinates in a photo’s metadata, formatting those coordinates, and making those coordinates more comprehensible by locating them on a map and converting them into the names of the country, region, and city where the photo was taken.

Getting the photo’s GPS coordinates

The EXIF format specifies a number of tags beginning with

gps
, which contain useful geolocation information, including the latitude and longitude where the photo was taken.

Enter the following into a new code cell and run it:

for index, image in enumerate(images):
    print(f"Coordinates - Image {index}")
    print("---------------------")
    print(f"Latitude: {image.gps_latitude} {image.gps_latitude_ref}")
    print(f"Longitude: {image.gps_longitude} {image.gps_longitude_ref}\n")

You should see these results:

Coordinates - Image 0
---------------------
Latitude: (28.0, 0.0, 1.5) N
Longitude: (82.0, 26.0, 58.7) W

Coordinates - Image 1
---------------------
Latitude: (28.0, 0.0, 1.1) N
Longitude: (82.0, 26.0, 58.99) W

Note that the

gps_latitude
and
gps_longitude
properties return the latitude and longitude as a tuple of three values, which are:

  1. Degrees
  2. Minutes (1/60th of a degree)
  3. Seconds (1/60th of a minute, or 1/3600th of a degree)

Latitude specifies the angular distance from the equator, which can be either north or south. The

gps_latitude_ref
property indicates this direction, which is either
N
or
S
.

Longitude specifies the angular distance from the meridian, which can be either east or west. The

gps_longitude_ref
property indicates this direction, which is either
E
or
W
.

You may have noticed the slight difference between the coordinates reported in the photos. This is expected; even the same device, located at the same spot, will report slightly different coordinates at different times. The discrepancy in coordinates reported by the phones is on the order of a fraction of a second, which translates to about 8 meters or about 25 feet.

Formatting latitude and longitude

Let’s define a couple of functions to format the latitude and longitude information returned by

Image
into standard formats:

  • Degrees, minutes, and seconds. In this format, the latitude of image 0 would be written as
    28.0° 0.0' 1.56" N
    .
  • Decimal degrees. In this format, the latitude of image 0 would be written as
    28.000433333333334
    . North latitudes and east longitudes are represented with positive values, while south latitudes and west longitudes are represented with negative values.

Here are the definitions for those functions, as well as some code that makes use of them — run the following in a new code cell:

def format_dms_coordinates(coordinates):
    return f"{coordinates[0]}° {coordinates[1]}\' {coordinates[2]}\""

def dms_coordinates_to_dd_coordinates(coordinates, coordinates_ref):
    decimal_degrees = coordinates[0] + \
                      coordinates[1] / 60 + \
                      coordinates[2] / 3600
    
    if coordinates_ref == "S" or coordinates_ref == "W":
        decimal_degrees = -decimal_degrees
    
    return decimal_degrees

for index, image in enumerate(images):
    print(f"Coordinates - Image {index}")
    print("---------------------")
    print(f"Latitude (DMS): {format_dms_coordinates(image.gps_latitude)} {image.gps_latitude_ref}")
    print(f"Longitude (DMS): {format_dms_coordinates(image.gps_longitude)} {image.gps_longitude_ref}\n")
    print(f"Latitude (DD): {dms_coordinates_to_dd_coordinates(image.gps_latitude, image.gps_latitude_ref)}")
    print(f"Longitude (DD): {dms_coordinates_to_dd_coordinates(image.gps_longitude, image.gps_longitude_ref)}\n")

Here’s the output:

Coordinates - Image 0
---------------------
Latitude (DMS): 28.0° 0.0' 1.5" N
Longitude (DMS): 82.0° 26.0' 58.7" W

Latitude (DD): 28.000416666666666
Longitude (DD): -82.4496388888889

Coordinates - Image 1
---------------------
Latitude (DMS): 28.0° 0.0' 1.1" N
Longitude (DMS): 82.0° 26.0' 58.99" W

Latitude (DD): 28.000305555555556
Longitude (DD): -82.44971944444445

Displaying photo locations on a map

You probably don’t know the latitude and longitude of your home, even when rounded to the nearest degree. There are a couple of simple ways to convert the EXIF location data into something easier for humans to understand.

One way is to use Python’s built-in webbrowser module to open a new browser tab for each photo, using the decimal version of each photo’s EXIF coordinates as the parameters for a Google Maps URL. We’ll create a utility function named

draw_map_for_location()
that does this.

Enter the following into a new code cell and run it:

def draw_map_for_location(latitude, latitude_ref, longitude, longitude_ref):
    import webbrowser
    
    decimal_latitude = dms_coordinates_to_dd_coordinates(latitude, latitude_ref)
    decimal_longitude = dms_coordinates_to_dd_coordinates(longitude, longitude_ref)
    url = f"https://www.google.com/maps?q={decimal_latitude},{decimal_longitude}"
    webbrowser.open_new_tab(url)

for index, image in enumerate(images):
    draw_map_for_location(image.gps_latitude, 
                          image.gps_latitude_ref, 
                          image.gps_longitude,
                          image.gps_longitude_ref)

When you run it, two new browser tabs will open, and each one will show a Google Map indicating where the corresponding photo was taken.

Displaying the city, region, and country where a photo was taken

Another way to display the location of a photo is to use reverse geocoding, which is the process of converting geographic coordinates into an address or the name of a place. Earlier, you installed the

reverse_geocoder
and
pycountry
modules; you’ll use them now.

Run the following in a new code cell. It will convert the photos’ coordinates into something more recognizable:

import reverse_geocoder as rg
import pycountry

for index, image in enumerate(images):
    print(f"Location info - Image {index}")
    print("-----------------------")
    decimal_latitude = dms_coordinates_to_dd_coordinates(image.gps_latitude, image.gps_latitude_ref)
    decimal_longitude = dms_coordinates_to_dd_coordinates(image.gps_longitude, image.gps_longitude_ref)
    coordinates = (decimal_latitude, decimal_longitude)
    location_info = rg.search(coordinates)[0]
    location_info['country'] = pycountry.countries.get(alpha_2=location_info['cc'])
    print(f"{location_info}\n")

It will produce the following output:

Location info - Image 0
-----------------------
{'lat': '27.94752', 'lon': '-82.45843', 'name': 'Tampa', 'admin1': 'Florida', 'admin2': 'Hillsborough County', 'cc': 'US', 'country': Country(alpha_2='US', alpha_3='USA', flag='🇺🇸', name='United States', numeric='840', official_name='United States of America')}

Location info - Image 1
-----------------------
{'lat': '27.94752', 'lon': '-82.45843', 'name': 'Tampa', 'admin1': 'Florida', 'admin2': 'Hillsborough County', 'cc': 'US', 'country': Country(alpha_2='US', alpha_3='USA', flag='🇺🇸', name='United States', numeric='840', official_name='United States of America')}

The code uses reversegeocoder_’s

search()
method to convert each photo’s decimal latitude and longitude into a collection of the following information:

  • City, town, or village name
  • Major administrative region, which is typically a state or province
  • Minor administrative region, which is typically a county or district
  • Country code

It then uses pycountry’s

get()
method to convert the country code provided by
reverse_geocoder
into a tuple containing the corresponding common and official country names.

What direction was the camera facing?

As I mentioned at the start of this article, smartphones are packed with sensors. These include a magnetometer, barometer, and accelerometer. In combination with GPS, these sensors provide extra information that is added to each photo’s EXIF metadata.

The magnetometer senses magnetic fields, including the giant one generated by the Earth. Its primary purpose is to be the phone’s compass and determine the direction in which the phone is pointing. That information is written into EXIF as a compass heading every time you take a picture.

Let’s determine which direction the camera was facing for each of the photos below:

Lake image, looking in one direction

Lake image, looking in another direction

Lake image, looking in yet another direction

We’ll use the following exif

Image
properties to determine the direction in which the camera was pointed:

  • gps_img_direction
    : The compass heading (that is, direction) that the camera was facing when the picture was taken, expressed in decimal degrees. 0° is north, 90° is east, 180° is south, and 270° is west.
  • gps_img_direction_ref
    : The reference point for
    gps_img_direction
    . This can be either
    T
    , which means that 0° refers to true or geographic north, or
    M
    , which means that 0° refers to magnetic north. Most of the time, true north is used.

The code below displays the camera direction for the four lake photos. It makes use of a couple of utility functions:

  • degrees_to_direction()
    : This function converts compass headings into cardinal directions (e.g., N, NE, NNE, and so on).
  • format_direction_ref()
    : This function turns the value in
    gps_img_direction_ref
    into a human-friendly string.

Run this code in a new code cell:

def degrees_to_direction(degrees):
    COMPASS_DIRECTIONS = [
        "N",
        "NNE",
        "NE",
        "ENE",
        "E", 
        "ESE", 
        "SE", 
        "SSE",
        "S", 
        "SSW", 
        "SW", 
        "WSW", 
        "W", 
        "WNW", 
        "NW", 
        "NNW"
    ]
    
    compass_directions_count = len(COMPASS_DIRECTIONS)
    compass_direction_arc = 360 / compass_directions_count
    return COMPASS_DIRECTIONS[int(degrees / compass_direction_arc) % compass_directions_count]

def format_direction_ref(direction_ref):
    direction_ref_text = "(true or magnetic north not specified)"
    if direction_ref == "T":
        direction_ref_text = "True north"
    elif direction_ref == "M":
        direction_ref_text = "Magnetic north"
    return direction_ref_text

# Import images
lake_images = []
for i in range(1, 4):
    filename = f"lake-{i}.jpg"
    with open(f"images/{filename}", "rb") as current_file:
        lake_images.append(Image(current_file))

# Display camera direction for each image
for index, image in enumerate(lake_images):
    print(f"Image direction - lake-{index + 1}")
    print("-------------------------")
    print(f"Image direction: {degrees_to_direction(image.gps_img_direction)} ({image.gps_img_direction}°)")
    print(f"Image direction ref: {format_direction_ref(image.gps_img_direction_ref)}\n")

It should produce this output:

Image direction - lake-1
-------------------------
Image direction: WNW (292.7559205500382°)
Image direction ref: True north

Image direction - lake-2
-------------------------
Image direction: NNW (351.0164186762442°)
Image direction ref: True north

Image direction - lake-3
-------------------------
Image direction: NE (53.7673950546291°)
Image direction ref: True north

At What Altitude Was the Photo Taken?

In addition to providing location coordinates, GPS can also determine altitude. Some smartphones have barometers (which detect air pressure), which they use to increase the accuracy of the altitude measurement.

Let’s find out the altitudes where these photos were taken:

One of the outdoor pools overlooking the beach at Shangri-La, Mactan, Philippines

The Chocolate Hills in Bohol, Philippines

We’ll use the following exif Image properties:

  • gps_altitude
    : The altitude reported by the camera, expressed in meters.
  • gps_altitude_ref
    : The reference point for
    gps_altitude
    . This value is either
    0
    , which means that the value in
    gps_altitude
    refers to meters above sea level, or
    1
    , which means that the value in
    gps_altitude
    refers to meters below sea level.

The following code displays the altitude reported by the phone at the moment each of the photos was taken. It utilizes one utility function,

format_altitude()
, which specifies whether the given altitude is above or below sea level.

Enter this into a new code cell and run it:

def format_altitude(altitude, altitude_ref):
    altitude_ref_text = "(above or below sea level not specified)"
    if altitude_ref == 0:
        altitude_ref_text = "above sea level"
    elif altitude_ref == 1:
        altitude_ref_text = "below sea level"
    return f"{altitude} meters {altitude_ref_text}"

# Import images
altitude_images = []
for i in range(1, 3):
    filename = f"altitude-{i}.jpg"
    with open(f"images/{filename}", "rb") as current_file:
        altitude_images.append(Image(current_file))
        
# Display camera altitude for each image
for index, image in enumerate(altitude_images):
    print(f"Altitude - altitude-{index + 1}")
    print( "------------------")
    print(f"{format_altitude(image.gps_altitude, image.gps_altitude_ref)}\n")

Here’s its resulting output:

Altitude - altitude-1
------------------
14.025835753108481 meters above sea level

Altitude - altitude-2
------------------
359.13079847908745 meters above sea level

I could tell you when and where I took these photos, but you already know how to find out for yourself.

Was the Photographer Moving?

Smartphones use a combination of GPS locations over time and the accelerometer to determine the phone’s speed and the direction in which it’s moving. Some phones provide this information as part of the EXIF metadata in photos.

If a phone adds speed-related metadata to the photos it takes — such as the iPhone — you can access this data via these two exif

Image
object methods:

  • gps_speed
    : The speed reported by the camera, expressed as a number.
  • gps_speed_ref
    : The speed units used for the value in
    gps_speed
    . This value can be
    K
    for kilometers per hour,
    M
    for miles per hour, or
    N
    for nautical miles per hour, or “knots”.

Consider the following photos:

Geese

View of a road as seen from a bicycle

View of a road as seen from a car

Here’s code that prints out the recorded speed at the time each photo was taken. It includes a utlility function,

format_speed_ref()
, which specifies the units of the reported speed. Enter it into a new code cell and run it:

def format_speed_ref(speed_ref):
    speed_ref_text = "(speed units not specified)"
    if speed_ref == "K":
        speed_ref_text = "km/h"
    elif speed_ref == "M":
        speed_ref_text = "mph"
    elif speed_ref == "N":
        speed_ref_text = "knots"
    return speed_ref_text

# Import images
speed_images = []
for i in range(1, 4):
    filename = f"speed-{i}.jpg"
    with open(f"images/{filename}", "rb") as current_file:
        speed_images.append(Image(current_file))
    
for index, image in enumerate(speed_images):
    print(f"Speed - speed-{index + 1}")
    print("---------------")
    print(f"Speed: {image.gps_speed} {format_speed_ref(image.gps_speed_ref)}\n")

Here are the speeds it reports:

Speed - speed-1
---------------
Speed: 0.09284484943898176 km/h

Speed - speed-2
---------------
Speed: 5.489431578947368 km/h

Speed - speed-3
---------------
Speed: 20.195413618458076 km/h

Updating EXIF Data and Saving It

So far, we’ve limited ourselves to simply reading the EXIF metadata from photos. The next step is to make changes to that data and then save the results as a new photo file.

Updating a photo’s coordinates

Let’s start with this photo:

“You Are Here” art display outside the Salt Palace in Salt Lake City

By now, you should know how to read its coordinates from EXIF and use those coordinates to open a Google Map to show the location they represent.

Here’s the code that will do just that. It makes use of the

draw_map_for_location()
utility function that we defined earlier:

with open(f"images/salt-palace.jpg", "rb") as salt_palace_file:
    salt_palace = Image(salt_palace_file)
    
# Read the GPS data
print("Original coordinates")
print("--------------------")
print(f"Latitude: {salt_palace.gps_latitude} {salt_palace.gps_latitude_ref}")
print(f"Longitude: {salt_palace.gps_longitude} {salt_palace.gps_longitude_ref}\n")

# Open a Google Map showing the location represented by these coordinates
draw_map_for_location(salt_palace.gps_latitude,
                      salt_palace.gps_latitude_ref,
                      salt_palace.gps_longitude,
                      salt_palace.gps_longitude_ref)

It outputs the following...

Original coordinates
--------------------
Latitude: (40.0, 46.0, 1.87) N
Longitude: (111.0, 53.0, 39.16) W

...and it opens a new browser tab showing a Google Map that displays the Salt Palace Convention Center in downtown Salt Lake City, Utah.

Let’s make things a little more interesting by changing the coordinates embedded in the photo’s EXIF data so that it reports that it was taken at Area 51. In case you haven’t heard of this place, it’s a military installation in Nevada, where conspiracy theorists believe that the U.S. government stores the bodies of aliens and a spaceship that were captured in the 1950s. Its coordinates are 37.0° 14' 3.6" N, 115° 48' 23.99" W.

You've seen that reading an EXIF tag’s value using exif’s

Image
object is simply a matter of reading the value in its corresponding property. Likewise, updating its value is simply a matter of assigning a new value to that property. In this case, we’ll assign values that specify Area 51’s coordinates to the image’s
gps_latitude
,
gps_latitude_ref
,
gps_longitude
, and
gps_longitude_ref
properties:

Revised coordinates
-------------------
Latitude: (37.0, 14.0, 3.6) N
Longitude: (115.0, 48.0, 23.99) W

Running the code will produce this output...

Revised coordinates
-------------------
Latitude: (37.0, 14.0, 3.6) N
Longitude: (115.0, 48.0, 23.99) W

...and it will open a new browser showing a Google Map that displays Area 51.

Filling Out Unused EXIF Tags

There are a lot of tags defined in the current EXIF specification, and most cameras fill out only a small subset of them. The exif module makes it possible to fill any EXIF tags that don’t have a value assigned to them.

Note: The exif module writes only to tags in the EXIF specification, and not additional tags included by vendors. The “official” tags in the EXIF specification are underlined.

You can assign a value to a tag outside the EXIF specification, even a tag that you made up. The Image object will store that value, but only for as long as the object remains in memory. You won’t be able to save non-EXIF tags or their values because the method that converts the data in the Image object for writing to a file only takes standard EXIF tags into account.

Here are a couple of EXIF tags that you might want to fill out, as they’re handy for managing a library of photos. Both of them take string values:

  • ImageDescription
    : A description of the photo. In exif’s
    Image
    objects, this is exposed as the
    mage_description
    property.
  • Copyright
    : A copyright notice for the photo. In exif’s
    Image
    objects, this is exposed as the
    copyright
    property.

Here’s some code that fills out these tags:

salt_palace.image_description = "The Salt Palace Hotel in Salt Lake City."
salt_palace.copyright = "Copyright 2024 (Your name here)"

print(f"Description: {salt_palace.image_description}")
print(f"Copyright: {salt_palace.copyright}")

Saving a Photo with Updated EXIF Data

Now that we’ve updated the EXIF data in the picture, let’s save it as a brand new file with the name

salt-palace-updated.jpg
:

with open('images/salt-palace-updated.jpg', 'wb') as updated_salt_palace_file:
    updated_salt_palace_file.write(salt_palace.get_file())

This code creates a file object,

updated_hotel_file
, to write binary data to a file named
salt-palace-updated.jpg
. It then uses
Image
’s
get_file()
method to get the image data in serializable form and writes that data to the file.

You should now have a new file:

salt-palace-updated.jpg
. Let’s load it and confirm that the data we modified and added were saved:

with open(f"images/salt-palace-updated.jpg", "rb") as salt_palace_updated_file:
    salt_palace_updated = Image(salt_palace_updated_file)
    
print("Coordinates")
print("-----------")
print(f"Latitude: {salt_palace_updated.gps_latitude} {salt_palace_updated.gps_latitude_ref}")
print(f"Longitude: {salt_palace_updated.gps_longitude} {salt_palace_updated.gps_longitude_ref}\n")

print("Other info")
print("----------")
print(f"Description: {salt_palace_updated.image_description}")
print(f"Copyright: {salt_palace_updated.copyright}")

# Open a Google Map showing the location represented by these coordinates
draw_map_for_location(salt_palace_updated.gps_latitude,
                      salt_palace_updated.gps_latitude_ref,
                      salt_palace_updated.gps_longitude,
                      salt_palace_updated.gps_longitude_ref)

The code will give you this output...

Coordinates
-----------
Latitude: (37.0, 14.0, 3.6) N
Longitude: (115.0, 48.0, 23.99) W

Other info
----------
Description: The Salt Palace Hotel in Salt Lake City.
Copyright: Copyright 2024 (Your name here)

Deleting EXIF Data and Saving the “Scrubbed” Photo

Suppose that instead of altering the hotel photo’s location data, we decided to delete its tags instead. There are three ways to do this.

The first way is to use the

delete()
method provided by exif’s
Image
object. Let’s use it to delete the latitude data:

salt_palace.delete('gps_latitude')
salt_palace.delete('gps_latitude_ref')

print("Latitude data")
print("-------------")
print(f"gps_latitude: {salt_palace.get('gps_latitude', 'Not found')}")
print(f"gps_latitude_ref: {salt_palace.get('gps_latitude_ref', 'Not found')}")

Here’s the output for the code above:

atitude data
-------------
gps_latitude: Not found
gps_latitude_ref: Not found

The second way is to use Python’s

del
statement, which deletes objects from memory. In this case, we’ll use it to delete the attributes of
hotel_image
that store the longitude data:

del salt_palace.gps_longitude
del salt_palace.gps_longitude_ref

print("Longitude data")
print("--------------")
print(f"gps_longitude: {salt_palace.get('gps_longitude', 'Not found')}")
print(f"gps_longitude_ref: {salt_palace.get('gps_longitude_ref', 'Not found')}")

Here’s the output for the code above:

Longitude data
--------------
gps_longitude: Not found
gps_longitude_ref: Not found

Now that we’ve removed the location data from the photo, let’s save it under a new filename,

salt-palace-no-location.jpg
:

with open('images/salt-palace-no-location.jpg', 'wb') as salt_palace_no_loc_file:
    salt_palace_no_loc_file.write(salt_palace.get_file())

Finally, if you want to simply delete all the EXIF data from the photo with a single function call, you can use the exif

Image
object’s
delete_all()
method:

salt_palace.delete_all()
dir(salt_palace)

You’ll see this result:

['<unknown EXIF tag 316>',
 '<unknown EXIF tag 322>',
 '<unknown EXIF tag 323>',
 '<unknown EXIF tag 42080>',
 '_exif_ifd_pointer',
 '_gps_ifd_pointer',
 '_segments',
 'delete',
 'delete_all',
 'exif_version',
 'get',
 'get_all',
 'get_file',
 'get_thumbnail',
 'has_exif',
 'list_all']

What remains are tags that the exif module doesn’t recognize and the

Image
methods and properties.

The code below saves the image as

salt-palace-deleted-tags.jpg
:

with open('images/salt-palace-deleted-tags.jpg', 'wb') as deleted_tags_file:
    deleted_tags_file.write(salt_palace.get_file())

Practical and Technical Considerations

Reasons to remove EXIF metadata

EXIF metadata adds a whole new level of information to digital photographs, and a number of things that users, developers, and organizations must consider.

One of the obvious considerations is that of privacy. A single photo’s GPS metadata can give away your location at a specific date and time. What’s far more valuable is the aggregate GPS from someone’s camera roll, as it contains patterns that can be analyzed to determine where they live, work, and what places you frequent on their daily routine. This is why the more reputable social networking services strip this information when you share your photos online. Remember that this only means that your photos on your social network gallery don’t contain GPS data. There’s no guarantee that the service didn’t record the location information for advertiser data mining purposes before removing it.

Other EXIF metadata, such as the combination of make, model, settings, and even preferred camera orientation, can be used to associate a set of photos with a specific person.

Some photographers are more concerned about secrecy than privacy when it comes to their photos’ metadata. They don’t want to give away the camera settings they used to take pictures.

Reasons to retain or add EXIF metadata

One of the original reasons why the EXIF metadata format was developed was to make it easier to automate the process of cataloging, sorting, and searching through a collection of digital photographs. EXIF makes it possible to search for photos taken during a certain time period, with a certain device, or at a certain location. Other tags, such as the

Copyright
tag, make it possible to search for photos taken by a specific photographer, as well as assert one’s copyright.

Although the

ImageDescription
tag was intended to store a description of the image, you can also use it to store other helpful text information related to the photo, such as tags to classify the image or instructions for processing and editing the photo.

EXIF metadata is particularly useful in mobile applications that record simultaneous combinations of visual, date/time, and location information. For example, a technician might be required to take “before” and “after” photos of repair work that they did, providing a record of where, when, and how the work was done.

Other things to consider

As we saw in the different tags recorded by the iPhone and Android devices used to take the photographs in this article, different cameras record different sets of EXIF tags. Photo editing software often writes information to different tags or adds their own custom tags. This additional metadata can often be a hint that a photo has been edited.

There’s a concept in wildlife photography called Ethical EXIF, which provides a description of the ethical standards followed when taking a photo. It includes information such as the health and stress level of the animal in the photo, how long the animal was in captivity, and if the animal was transported from its habitat for the picture.

Finally, there are the messages that camera-using mobile apps display when first used. They usually say something along the lines of “This app makes use of the camera. Is that all right with you?”, but most of them don’t make it clear that the camera adds location and other metadata to each photo it takes. If you write mobile apps that use the camera, you might want to inform the user of this.

Try out the most powerful authentication platform for free.

Get started →