Search code examples
pythonexifpiexif

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


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)

Solution

  • 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.