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:
- More people than ever have a camera that’s usually within arm’s reach.
- 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
: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
objects are in snakecase. This is because the authors of the exif_ module are striving to follow the Python Style Guide and designedImage
to convert EXIF tag names into Pythonic property names.Image
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:- Degrees
- Minutes (1/60th of a degree)
- 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
. North latitudes and east longitudes are represented with positive values, while south latitudes and west longitudes are represented with negative values.28.000433333333334
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:
We’ll use the following exif
Image
properties to determine the direction in which the camera was pointed:
: 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
: The reference point forgps_img_direction_ref
. This can be eithergps_img_direction
, which means that 0° refers to true or geographic north, orT
, which means that 0° refers to magnetic north. Most of the time, true north is used.M
The code below displays the camera direction for the four lake photos. It makes use of a couple of utility functions:
: This function converts compass headings into cardinal directions (e.g., N, NE, NNE, and so on).degrees_to_direction()
: This function turns the value informat_direction_ref()
into a human-friendly string.gps_img_direction_ref
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:
We’ll use the following exif Image properties:
: The altitude reported by the camera, expressed in meters.gps_altitude
: The reference point forgps_altitude_ref
. This value is eithergps_altitude
, which means that the value in0
refers to meters above sea level, orgps_altitude
, which means that the value in1
refers to meters below sea level.gps_altitude
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:
: The speed reported by the camera, expressed as a number.gps_speed
: The speed units used for the value ingps_speed_ref
. This value can begps_speed
for kilometers per hour,K
for miles per hour, orM
for nautical miles per hour, or “knots”.N
Consider the following photos:
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:
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:
: A description of the photo. In exif’sImageDescription
objects, this is exposed as theImage
property.mage_description
: A copyright notice for the photo. In exif’sCopyright
objects, this is exposed as theImage
property.copyright
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 →About the author
Joey deVilla
Senior Developer Advocate