|
6 | 6 | import subprocess |
7 | 7 | import traceback |
8 | 8 | from PIL import Image |
9 | | -import PIL.ExifTags |
| 9 | +from PIL.ExifTags import TAGS, GPSTAGS |
10 | 10 | from address import shard |
11 | | - |
12 | 11 | logger = logging.getLogger('metadb') |
13 | 12 | dir_mode = 0o700 # default directory creation mode |
14 | 13 |
|
|
19 | 18 | # /usr/bin/identify (image magick) |
20 | 19 |
|
21 | 20 | def _check_add_line(lines, line): |
| 21 | + """Add a unique line to the metadata lines list.""" |
22 | 22 | if line not in lines: |
23 | 23 | 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 |
25 | 35 |
|
26 | 36 | 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 |
32 | 96 |
|
33 | 97 | def _handle_exif(lines, image, metapath): |
| 98 | + """Process EXIF data from an image and append to metadata lines.""" |
34 | 99 | 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 | + |
80 | 125 | except Exception as error: |
81 | 126 | print("Exception: '{}', {}".format(metapath, error)) |
82 | 127 | traceback.print_exc() |
|
0 commit comments