Skip to content

Instantly share code, notes, and snippets.

@peyanski
Created October 13, 2026 08:42
Show Gist options
  • Select an option

  • Save peyanski/c9930c57a168b816eb7735d86554fce7 to your computer and use it in GitHub Desktop.

Select an option

Save peyanski/c9930c57a168b816eb7735d86554fce7 to your computer and use it in GitHub Desktop.
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