Skip to content

Commit 11e04e2

Browse files
committed
fix: Handle IFDRational correctly
1 parent 7e6e60b commit 11e04e2

File tree

2 files changed

+99
-53
lines changed

2 files changed

+99
-53
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ packages/
1010
/src/*.vsp
1111
/src/*.psess
1212
/src/*.user
13+
doc/

docker/cas/src/metadb.py

Lines changed: 98 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66
import subprocess
77
import traceback
88
from PIL import Image
9-
import PIL.ExifTags
9+
from PIL.ExifTags import TAGS, GPSTAGS
1010
from address import shard
11-
1211
logger = logging.getLogger('metadb')
1312
dir_mode = 0o700 # default directory creation mode
1413

@@ -19,64 +18,110 @@
1918
# /usr/bin/identify (image magick)
2019

2120
def _check_add_line(lines, line):
21+
"""Add a unique line to the metadata lines list."""
2222
if line not in lines:
2323
lines.append(line)
24-
#print("..Adding to metadata: ", line)
24+
25+
def _convert_to_float(value):
26+
"""Convert a rational number (tuple or IFDRational) to a float."""
27+
try:
28+
if isinstance(value, tuple) and len(value) == 2:
29+
return float(value[0]) / float(value[1])
30+
elif hasattr(value, 'numerator') and hasattr(value, 'denominator'):
31+
return float(value.numerator) / float(value.denominator)
32+
return float(value)
33+
except (TypeError, ZeroDivisionError, AttributeError):
34+
return None
2535

2636
def _convert_to_degrees(value):
27-
get_float = lambda x: float(x[0]) / float(x[1])
28-
degrees = get_float(value[0])
29-
minutes = get_float(value[1])
30-
seconds = get_float(value[2])
31-
return round(degrees + (minutes / 60.0) + (seconds / 3600.0), 6)
37+
"""Convert GPS coordinates from (degrees, minutes, seconds) to decimal degrees."""
38+
if not isinstance(value, (list, tuple)) or len(value) != 3:
39+
return None
40+
try:
41+
degrees = _convert_to_float(value[0])
42+
minutes = _convert_to_float(value[1])
43+
seconds = _convert_to_float(value[2])
44+
if None in (degrees, minutes, seconds):
45+
return None
46+
return round(degrees + (minutes / 60.0) + (seconds / 3600.0), 6)
47+
except (TypeError, IndexError):
48+
return None
49+
50+
def _format_tag_value(tag_name, value):
51+
"""Format EXIF tag value based on tag-specific rules."""
52+
units = {
53+
'SubjectDistance': 'm',
54+
'FocalLength': 'mm',
55+
'FlashEnergy': 'BCPS',
56+
'ExposureTime': 's',
57+
}
58+
if tag_name in units and value is not None:
59+
value = _convert_to_float(value)
60+
return f"{value} {units[tag_name]}" if value is not None else None
61+
return str(value) if value is not None else None
62+
63+
def _handle_gps_info(gps_info):
64+
"""Extract and format GPS information."""
65+
if not isinstance(gps_info, dict):
66+
return []
67+
68+
gps_data = {GPSTAGS.get(tag, tag): value for tag, value in gps_info.items()}
69+
lines = []
70+
71+
# Latitude
72+
if 'GPSLatitude' in gps_data and 'GPSLatitudeRef' in gps_data:
73+
latitude = _convert_to_degrees(gps_data['GPSLatitude'])
74+
lat_ref = gps_data['GPSLatitudeRef']
75+
if latitude is not None:
76+
latitude = -latitude if lat_ref == 'S' else latitude
77+
_check_add_line(lines, f"exif:GPSLatitude={latitude}{lat_ref}")
78+
79+
# Longitude
80+
if 'GPSLongitude' in gps_data and 'GPSLongitudeRef' in gps_data:
81+
longitude = _convert_to_degrees(gps_data['GPSLongitude'])
82+
lon_ref = gps_data['GPSLongitudeRef']
83+
if longitude is not None:
84+
longitude = -longitude if lon_ref == 'W' else longitude
85+
_check_add_line(lines, f"exif:GPSLongitude={longitude}{lon_ref}")
86+
87+
# Altitude
88+
if 'GPSAltitude' in gps_data:
89+
altitude = _convert_to_float(gps_data['GPSAltitude'])
90+
if altitude is not None:
91+
if 'GPSAltitudeRef' in gps_data and gps_data['GPSAltitudeRef'] != 0:
92+
altitude = -altitude
93+
_check_add_line(lines, f"exif:GPSAltitude={altitude}")
94+
95+
return lines
3296

3397
def _handle_exif(lines, image, metapath):
98+
"""Process EXIF data from an image and append to metadata lines."""
3499
try:
35-
exif = image._getexif()
36-
if exif:
37-
for tag, value in exif.items():
38-
if tag not in [37500, 50341, 37510, 282, 283, 40961, 296, 531]:
39-
if tag == 271: # Make
40-
_check_add_line(lines, "exif:Make={}".format(value))
41-
if tag == 272: # Model
42-
_check_add_line(lines, "exif:Model={}".format(value))
43-
if tag == 274: # Orientation
44-
_check_add_line(lines, "exif:Orientation={}".format(value))
45-
if tag == 36867: # DateTimeOriginal
46-
_check_add_line(lines, "exif:DateTimeOriginal={}".format(value))
47-
if tag == 36868: # DateTimeDigitized
48-
_check_add_line(lines, "exif:DateTimeDigitized={}".format(value))
49-
if tag == 37382: # SubjectDistance
50-
_check_add_line(lines, "exif:SubjectDistance={}/{} m".format(*value))
51-
if tag == 37385: # Flash
52-
_check_add_line(lines, "exif:Flash={}".format(value))
53-
if tag == 41483: # FlashEnergy
54-
_check_add_line(lines, "exif:FlashEnergy={}/{} BCPS".format(*value))
55-
if tag == 37386: # FocalLength
56-
_check_add_line(lines, "exif:FocalLength={}/{} mm".format(*value))
57-
if tag == 33434: # ExposureTime
58-
_check_add_line(lines, "exif:ExposureTime={}/{} s".format(*value))
59-
if tag == 33437: # FNumber
60-
_check_add_line(lines, "exif:FNumber={}/{}".format(*value))
61-
62-
if tag == 34853 and value: # GPSInfo
63-
if 2 in value: # not all devices provide latitude
64-
gps_latitude = _convert_to_degrees(value[2])
65-
gps_latitude_ref = value[1]
66-
_check_add_line(lines, "exif:GPSLatitude={}{}".format(gps_latitude, gps_latitude_ref))
67-
if 4 in value: # not all devices provide longitude
68-
gps_longitude = _convert_to_degrees(value[4])
69-
gps_longitude_ref = value[3]
70-
_check_add_line(lines, "exif:GPSLongitude={}{}".format(gps_longitude, gps_longitude_ref))
71-
if 6 in value: # not all devices provide altitude
72-
gps_altitude = round(float(value[6][0]) / float(value[6][1]))
73-
if 5 in value and value[5] != b'\x00':
74-
gps_altitude = -gps_altitude
75-
_check_add_line(lines, "exif:GPSAltitude={}".format(gps_altitude))
76-
77-
#if tag not in [271, 272, 274, 33434, 33437, 36867, 36868, 37382, 37385, 37386, 41483]:
78-
# decoded = PIL.ExifTags.TAGS.get(tag, tag)
79-
# print('tag=', tag, " decoded=", decoded, '->', value)
100+
exif_data = image._getexif()
101+
if not exif_data:
102+
return
103+
104+
# Map EXIF tag IDs to names
105+
exif = {TAGS.get(tag, tag): value for tag, value in exif_data.items()}
106+
107+
# Tags to process with specific formatting
108+
tag_formats = [
109+
'Make', 'Model', 'Orientation', 'DateTimeOriginal', 'DateTimeDigitized',
110+
'SubjectDistance', 'Flash', 'FlashEnergy', 'FocalLength', 'ExposureTime', 'FNumber'
111+
]
112+
113+
# Process standard EXIF tags
114+
for tag_name in tag_formats:
115+
if tag_name in exif:
116+
formatted_value = _format_tag_value(tag_name, exif[tag_name])
117+
if formatted_value:
118+
_check_add_line(lines, f"exif:{tag_name}={formatted_value}")
119+
120+
# Process GPSInfo separately
121+
if 'GPSInfo' in exif:
122+
gps_lines = _handle_gps_info(exif['GPSInfo'])
123+
lines.extend(gps_lines)
124+
80125
except Exception as error:
81126
print("Exception: '{}', {}".format(metapath, error))
82127
traceback.print_exc()

0 commit comments

Comments
 (0)