Adding EXIF GPS data to .jpg files using Python and Piexif

688 Views Asked by At

I am trying to write a script that adds EXIF GPS data to images using Python. When running the below script, I am getting an error returned from the piexif.dump() as follows:

(venv) C:\projects\geo-photo>python test2.py
Traceback (most recent call last):
  File "C:\projects\geo-photo\test2.py", line 31, in <module>
    add_geolocation(image_path, latitude, longitude)
  File "C:\projects\geo-photo\test2.py", line 21, in add_geolocation
    exif_bytes = piexif.dump(exif_dict)
                 ^^^^^^^^^^^^^^^^^^^^^^
  File "C:\projects\geo-photo\venv\Lib\site-packages\piexif\_dump.py", line 74, in dump
    gps_set = _dict_to_bytes(gps_ifd, "GPS", zeroth_length + exif_length)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\projects\geo-photo\venv\Lib\site-packages\piexif\_dump.py", line 335, in _dict_to_bytes
    length_str, value_str, four_bytes_over = _value_to_bytes(raw_value,
                                             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\projects\geo-photo\venv\Lib\site-packages\piexif\_dump.py", line 244, in _value_to_bytes
    new_value += (struct.pack(">L", num) +
struct.error: argument out of range

Does anyone have any idea as to why this would be happening? Below is the full script. Any help appreciated.

import piexif

def add_geolocation(image_path, latitude, longitude):
    exif_dict = piexif.load(image_path)

    # Convert latitude and longitude to degrees, minutes, seconds format
    def deg_to_dms(deg):
        d = int(deg)
        m = int((deg - d) * 60)
        s = int(((deg - d) * 60 - m) * 60)
        return ((d, 1), (m, 1), (s, 1))

    lat_dms = deg_to_dms(latitude)
    lon_dms = deg_to_dms(longitude)

    exif_dict["GPS"][piexif.GPSIFD.GPSLatitude] = lat_dms
    exif_dict["GPS"][piexif.GPSIFD.GPSLongitude] = lon_dms
    exif_dict["GPS"][piexif.GPSIFD.GPSLatitudeRef] = 'N' if latitude >= 0 else 'S'
    exif_dict["GPS"][piexif.GPSIFD.GPSLongitudeRef] = 'E' if longitude >= 0 else 'W'

    exif_bytes = piexif.dump(exif_dict)
    piexif.insert(exif_bytes, image_path)

    print("Geolocation data added to", image_path)

# Example usage
latitude = 34.0522  # Example latitude coordinates
longitude = -118.2437  # Example longitude coordinates
image_path = 'test.jpg'  # Path to your image

add_geolocation(image_path, latitude, longitude)
2

There are 2 best solutions below

0
Life is complex On BEST ANSWER

ExifTool by Phil Harvey will handle negative coordinates, such as -118.2437, but piexif has an issue with negative coordinates.

The line lon_dms = deg_to_dms(longitude) in your code produces the output ((-118, 1), (-14, 1), (-37, 1)). The negative values in this nested tuple cause a problem when calling this line of code exif_bytes = piexif.dump(exif_dict)

In the code below the negative coordinates are removed in the function deg_to_dms. The values that are produced by that function need to be converted into a format that piexif can use, which is accomplished in the function dms_to_exif_format.

The code below still needs some additional error handling and maybe some logging to be more production ready.

import piexif
from fractions import Fraction

def deg_to_dms(decimal_coordinate, cardinal_directions):
    """
    This function converts decimal coordinates into the DMS (degrees, minutes and seconds) format.
    It also determines the cardinal direction of the coordinates.

    :param decimal_coordinate: the decimal coordinates, such as 34.0522
    :param cardinal_directions: the locations of the decimal coordinate, such as ["S", "N"] or ["W", "E"]
    :return: degrees, minutes, seconds and compass_direction
    :rtype: int, int, float, string
    """
    if decimal_coordinate < 0:
        compass_direction = cardinal_directions[0]
    elif decimal_coordinate > 0:
        compass_direction = cardinal_directions[1]
    else:
        compass_direction = ""
    degrees = int(abs(decimal_coordinate))
    decimal_minutes = (abs(decimal_coordinate) - degrees) * 60
    minutes = int(decimal_minutes)
    seconds = Fraction((decimal_minutes - minutes) * 60).limit_denominator(100)
    return degrees, minutes, seconds, compass_direction

def dms_to_exif_format(dms_degrees, dms_minutes, dms_seconds):
    """
    This function converts DMS (degrees, minutes and seconds) to values that can
    be used with the EXIF (Exchangeable Image File Format).

    :param dms_degrees: int value for degrees
    :param dms_minutes: int value for minutes
    :param dms_seconds: fractions.Fraction value for seconds
    :return: EXIF values for the provided DMS values
    :rtype: nested tuple
    """
    exif_format = (
        (dms_degrees, 1),
        (dms_minutes, 1),
        (int(dms_seconds.limit_denominator(100).numerator), int(dms_seconds.limit_denominator(100).denominator))
    )
    return exif_format


def add_geolocation(image_path, latitude, longitude):
    """
    This function adds GPS values to an image using the EXIF format.
    This fumction calls the functions deg_to_dms and dms_to_exif_format.

    :param image_path: image to add the GPS data to
    :param latitude: the north–south position coordinate
    :param longitude: the east–west position coordinate
    """
    # converts the latitude and longitude coordinates to DMS
    latitude_dms = deg_to_dms(latitude, ["S", "N"])
    longitude_dms = deg_to_dms(longitude, ["W", "E"])

    # convert the DMS values to EXIF values
    exif_latitude = dms_to_exif_format(latitude_dms[0], latitude_dms[1], latitude_dms[2])
    exif_longitude = dms_to_exif_format(longitude_dms[0], longitude_dms[1], longitude_dms[2])

    try:
        # Load existing EXIF data
        exif_data = piexif.load(image_path)

        # https://exiftool.org/TagNames/GPS.html
        # Create the GPS EXIF data
        coordinates = {
            piexif.GPSIFD.GPSVersionID: (2, 0, 0, 0),
            piexif.GPSIFD.GPSLatitude: exif_latitude,
            piexif.GPSIFD.GPSLatitudeRef: latitude_dms[3],
            piexif.GPSIFD.GPSLongitude: exif_longitude,
            piexif.GPSIFD.GPSLongitudeRef: longitude_dms[3]
        }

        # Update the EXIF data with the GPS information
        exif_data['GPS'] = coordinates

        # Dump the updated EXIF data and insert it into the image
        exif_bytes = piexif.dump(exif_data)
        piexif.insert(exif_bytes, image_path)
        print(f"EXIF data updated successfully for the image {image_path}.")
    except Exception as e:
        print(f"Error: {str(e)}")


latitude = 34.0522
longitude = -118.2437
image_path = '_DSC0075.jpeg'  # Path to your image
add_geolocation(image_path, latitude, longitude)

Here is the original image without the GPS data: enter image description here

Here is the modified image with the GPS data: enter image description here

Here is an online utility that is useful for checking conversions from decimal coordinates to DMS (degrees, minutes and seconds) ones. There is also one that reverses the process.

1
TDG On

The source of the problem is negative longitude/latitude values - piexif is converting the supplied data into bytes format, and the line that causes the error - struct.pack(">L", num) is expecting unsigned values, as suggested by the L parameter.
From the discussion here you can see that negative value was converted to positive before adding it to the exif. Also found this exemple that converts negative values to positive ones, whie maintaining the right half/hemishpere - N/S or E/W. I don't know why the module is not using negative values - some EXIF readers will read also the N/S E/W values like this one, while others will ignore it - like the Windows built-in reader that you get by right clicking the image -> properties -> details.