SXXXXXXX_ControlPanel/VideoReceiverSFP/core/metadata_formatter.py
2026-01-16 10:09:51 +01:00

146 lines
5.8 KiB
Python

"""Formatting helpers for ImageLeaderData and nested metadata structures.
Provides `format_image_leader(leader)` which returns a human-readable dict
and `pretty_print_image_leader(leader)` which returns a multiline string.
"""
from typing import Any, Dict, Optional
import math
def _safe_get(obj: Any, attr: str, default: Any = None) -> Any:
try:
return getattr(obj, attr)
except Exception:
return default
def _bytes_to_hex(b: Any) -> str:
try:
# supports ctypes arrays of c_ubyte or bytes
return ''.join(f"{int(x):02X}" for x in b)
except Exception:
try:
return bytes(b).hex().upper()
except Exception:
return str(b)
def _rad_to_deg(rad: Optional[float]) -> Optional[float]:
if rad is None:
return None
try:
return float(rad) * (180.0 / math.pi)
except Exception:
return None
def format_image_leader(leader: Any) -> Dict[str, Any]:
"""Return a dict with human-friendly fields extracted from `leader`.
Accepts either a ctypes ImageLeaderData-like object or a dict-like structure.
"""
out: Dict[str, Any] = {}
if leader is None:
return out
# HEADER_TAG.ID (two bytes)
header_tag = _safe_get(leader, 'HEADER_TAG', None)
if header_tag is not None:
out['HEADER_TAG_ID'] = _bytes_to_hex(_safe_get(header_tag, 'ID', b''))
out['HEADER_TAG_VALID'] = int(_safe_get(header_tag, 'VALID', 0))
out['HEADER_TAG_SIZE'] = int(_safe_get(header_tag, 'SIZE', 0))
# HEADER_DATA (main header)
hd = _safe_get(leader, 'HEADER_DATA', None)
if hd is not None:
out['TYPE'] = int(_safe_get(hd, 'TYPE', 0))
out['SUBTYPE'] = int(_safe_get(hd, 'SUBTYPE', 0))
out['FCOUNTER'] = int(_safe_get(hd, 'FCOUNTER', 0))
out['TIMETAG'] = int(_safe_get(hd, 'TIMETAG', 0))
out['DX'] = int(_safe_get(hd, 'DX', 0))
out['DY'] = int(_safe_get(hd, 'DY', 0))
out['STRIDE'] = int(_safe_get(hd, 'STRIDE', 0))
out['BPP'] = int(_safe_get(hd, 'BPP', 0))
out['PALTYPE'] = int(_safe_get(hd, 'PALTYPE', 0))
# PRODINFO often is an array of two unsigned longs; present hex and ints
prodinfo = _safe_get(hd, 'PRODINFO', None)
if prodinfo is not None:
try:
out['PRODINFO'] = [int(x) for x in prodinfo]
except Exception:
out['PRODINFO'] = str(prodinfo)
# TOD may be two ushorts
tod = _safe_get(hd, 'TOD', None)
if tod is not None:
try:
out['TOD'] = [int(x) for x in tod]
except Exception:
out['TOD'] = str(tod)
# GEO_TAG / GEO_DATA
geo_tag = _safe_get(leader, 'GEO_TAG', None)
if geo_tag is not None:
out['GEO_TAG_ID'] = _bytes_to_hex(_safe_get(geo_tag, 'ID', b''))
out['GEO_TAG_VALID'] = int(_safe_get(geo_tag, 'VALID', 0))
out['GEO_TAG_SIZE'] = int(_safe_get(geo_tag, 'SIZE', 0))
geo = _safe_get(leader, 'GEO_DATA', None)
if geo is not None:
out['ORIENTATION_rad'] = float(_safe_get(geo, 'ORIENTATION', 0.0))
out['ORIENTATION_deg'] = _rad_to_deg(_safe_get(geo, 'ORIENTATION', 0.0))
out['LATITUDE_rad'] = float(_safe_get(geo, 'LATITUDE', 0.0))
out['LATITUDE_deg'] = _rad_to_deg(_safe_get(geo, 'LATITUDE', 0.0))
out['LONGITUDE_rad'] = float(_safe_get(geo, 'LONGITUDE', 0.0))
out['LONGITUDE_deg'] = _rad_to_deg(_safe_get(geo, 'LONGITUDE', 0.0))
out['REF_X'] = int(_safe_get(geo, 'REF_X', 0))
out['REF_Y'] = int(_safe_get(geo, 'REF_Y', 0))
out['SCALE_X'] = float(_safe_get(geo, 'SCALE_X', 0.0))
out['SCALE_Y'] = float(_safe_get(geo, 'SCALE_Y', 0.0))
# PIXEL_TAG
pixel_tag = _safe_get(leader, 'PIXEL_TAG', None)
if pixel_tag is not None:
out['PIXEL_TAG_ID'] = _bytes_to_hex(_safe_get(pixel_tag, 'ID', b''))
out['PIXEL_TAG_VALID'] = int(_safe_get(pixel_tag, 'VALID', 0))
out['PIXEL_TAG_SIZE'] = int(_safe_get(pixel_tag, 'SIZE', 0))
return out
def pretty_print_image_leader(leader: Any) -> str:
d = format_image_leader(leader)
lines = []
if not d:
return "<no leader>"
lines.append(f"HEADER_TAG.ID: {d.get('HEADER_TAG_ID')}")
lines.append(f"HEADER_TAG.VALID: {d.get('HEADER_TAG_VALID')}")
lines.append(f"HEADER_TAG.SIZE: {d.get('HEADER_TAG_SIZE')}")
lines.append("")
lines.append(f"TYPE: {d.get('TYPE')}")
lines.append(f"SUBTYPE: {d.get('SUBTYPE')}")
lines.append(f"FCOUNTER: {d.get('FCOUNTER')}")
lines.append(f"TIMETAG: {d.get('TIMETAG')}")
lines.append(f"DXxDY: {d.get('DX')} x {d.get('DY')}")
lines.append(f"STRIDE: {d.get('STRIDE')}")
lines.append(f"BPP: {d.get('BPP')}")
lines.append(f"PALTYPE: {d.get('PALTYPE')}")
if 'PRODINFO' in d:
lines.append(f"PRODINFO: {d.get('PRODINFO')}")
if 'TOD' in d:
lines.append(f"TOD: {d.get('TOD')}")
lines.append("")
if 'GEO_TAG_ID' in d:
lines.append(f"GEO_TAG.ID: {d.get('GEO_TAG_ID')}")
lines.append(f"GEO_TAG.VALID: {d.get('GEO_TAG_VALID')}")
lines.append(f"GEO_TAG.SIZE: {d.get('GEO_TAG_SIZE')}")
if 'ORIENTATION_deg' in d:
lines.append(f"ORIENTATION: {d.get('ORIENTATION_deg'):.4f} deg ({d.get('ORIENTATION_rad'):.6f} rad)")
if 'LATITUDE_deg' in d and 'LONGITUDE_deg' in d:
lines.append(f"LAT,LON: {d.get('LATITUDE_deg'):.6f} deg, {d.get('LONGITUDE_deg'):.6f} deg")
if 'REF_X' in d:
lines.append(f"REF_X,REF_Y: {d.get('REF_X')}, {d.get('REF_Y')}")
if 'SCALE_X' in d:
lines.append(f"SCALE_X,SCALE_Y: {d.get('SCALE_X'):.6f}, {d.get('SCALE_Y'):.6f}")
if 'PIXEL_TAG_ID' in d:
lines.append(f"PIXEL_TAG.ID: {d.get('PIXEL_TAG_ID')} VALID={d.get('PIXEL_TAG_VALID')} SIZE={d.get('PIXEL_TAG_SIZE')}")
return "\n".join(lines)