"""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 "" 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)