Created
October 13, 2026 08:42
-
-
Save peyanski/c9930c57a168b816eb7735d86554fce7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import os | |
| import sys | |
| import json | |
| import struct | |
| import io | |
| import urllib.request | |
| def parse_exif(source): | |
| out = {"has_gps": False, "latitude": 0.0, "longitude": 0.0, "timestamp": "Unknown"} | |
| try: | |
| # Resolve internal Home Assistant proxy URLs | |
| if source.startswith('/api/'): | |
| source = 'http://localhost:8123' + source | |
| if source.startswith('http://') or source.startswith('https://'): | |
| req = urllib.request.urlopen(source) | |
| data_bytes = req.read() | |
| f = io.BytesIO(data_bytes) | |
| else: | |
| if not os.path.exists(source): | |
| out["error"] = f"File not found: {source}" | |
| return out | |
| f = open(source, 'rb') | |
| with f: | |
| # Verify JPEG signature | |
| if f.read(2) != b'\xff\xd8': | |
| out["error"] = "Not a valid JPEG" | |
| return out | |
| # Locate the APP1 EXIF marker block | |
| while True: | |
| head = f.read(4) | |
| if len(head) < 4: break | |
| marker, length = struct.unpack('>HH', head) | |
| if marker == 0xffe1: | |
| data = f.read(length - 2) | |
| if data[:6] == b'Exif\x00\x00': | |
| decode_tiff(data[6:], out) | |
| break | |
| elif marker in [0xffda, 0xffd9]: # Image data start or EOF | |
| break | |
| else: | |
| f.seek(length - 2, 1) | |
| except Exception as e: | |
| out["error"] = str(e) | |
| return out | |
| def decode_tiff(tiff_data, out): | |
| endian = '<' if tiff_data[:2] == b'II' else '>' | |
| if tiff_data[2:4] != (b'\x2a\x00' if endian == '<' else b'\x00\x2a'): | |
| return | |
| ifd0_offset = struct.unpack_from(endian + 'I', tiff_data, 4)[0] | |
| gps_offset = None | |
| pos = ifd0_offset | |
| num_entries = struct.unpack_from(endian + 'H', tiff_data, pos)[0] | |
| pos += 2 | |
| for _ in range(num_entries): | |
| tag, dtype, count, value_offset = struct.unpack_from(endian + 'HHII', tiff_data, pos) | |
| pos += 12 | |
| if tag == 0x8825: # GPS directory pointer | |
| gps_offset = value_offset | |
| # Scan for standard YYYY:MM:DD date string | |
| idx = tiff_data.find(b'202') | |
| if idx != -1 and len(tiff_data) >= idx + 19: | |
| ts = tiff_data[idx:idx+19].decode('utf-8', errors='ignore') | |
| if ":" in ts and " " in ts: | |
| out["timestamp"] = ts | |
| # Parse GPS sub-block | |
| if gps_offset is not None: | |
| try: | |
| gps_pos = gps_offset | |
| num_gps_entries = struct.unpack_from(endian + 'H', tiff_data, gps_pos)[0] | |
| gps_pos += 2 | |
| gps_tags = {} | |
| for _ in range(num_gps_entries): | |
| tag, dtype, count, val_offset = struct.unpack_from(endian + 'HHII', tiff_data, gps_pos) | |
| gps_pos += 12 | |
| gps_tags[tag] = (dtype, count, val_offset) | |
| def get_rational_val(tag_idx): | |
| dtype, count, off = gps_tags[tag_idx] | |
| d_num, d_den = struct.unpack_from(endian + 'II', tiff_data, off) | |
| m_num, m_den = struct.unpack_from(endian + 'II', tiff_data, off + 8) | |
| s_num, s_den = struct.unpack_from(endian + 'II', tiff_data, off + 16) | |
| deg = float(d_num) / d_den if d_den != 0 else 0 | |
| m_deg = float(m_num) / m_den if m_den != 0 else 0 | |
| s_deg = float(s_num) / s_den if s_den != 0 else 0 | |
| return deg + (m_deg / 60.0) + (s_deg / 3600.0) | |
| if 2 in gps_tags: | |
| lat = get_rational_val(2) | |
| if 1 in gps_tags: | |
| ref_byte = struct.pack(endian + 'I', gps_tags[1][2])[:1].decode('utf-8', errors='ignore') | |
| if ref_byte in ['S', 's']: lat = -lat | |
| out["latitude"] = round(lat, 6) | |
| out["has_gps"] = True | |
| if 4 in gps_tags: | |
| lon = get_rational_val(4) | |
| if 3 in gps_tags: | |
| ref_byte = struct.pack(endian + 'I', gps_tags[3][2])[:1].decode('utf-8', errors='ignore') | |
| if ref_byte in ['W', 'w']: lon = -lon | |
| out["longitude"] = round(lon, 6) | |
| out["has_gps"] = True | |
| except: | |
| pass | |
| image_path = sys.argv[1] if len(sys.argv) > 1 else "/media/collection_snapshot.jpeg" | |
| print(json.dumps(parse_exif(image_path))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment