"""Collect descriptive metadata about a source image for the on-disk BASIC info program: name, dimensions, format, colour depth, EXIF dates/comments, the file's own date, when the C64 version was made, and the host platform. """ from __future__ import annotations import datetime import os import platform from PIL import Image _DEPTH = { "1": "1 bit mono", "L": "8 bit gray", "LA": "8 bit gray+a", "P": "8 bit palette", "PA": "8 bit pal+a", "RGB": "24 bit rgb", "RGBA": "32 bit rgba", "RGBX": "32 bit rgb", "CMYK": "32 bit cmyk", "YCbCr": "24 bit ycc", "I": "32 bit int", "F": "32 bit float", "I;16": "16 bit gray", } # EXIF tag ids. _DATETIME = 306 _DATETIME_ORIGINAL = 36867 _DATETIME_DIGITIZED = 36868 _IMAGE_DESCRIPTION = 270 _USER_COMMENT = 37510 _XP_COMMENT = 40092 _EXIF_IFD = 0x8769 def _fmt_ts(ts) -> str: return datetime.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M") def _parse_exif_dt(value) -> datetime.datetime | None: if isinstance(value, bytes): value = value.decode("ascii", "ignore") try: return datetime.datetime.strptime(str(value).strip(), "%Y:%m:%d %H:%M:%S") except (ValueError, TypeError): return None def _decode_comment(value) -> str | None: if not value: return None if isinstance(value, bytes): raw = value # EXIF UserComment has an 8-byte charset prefix (ASCII / UNICODE / ...). if raw[:8] in (b"ASCII\x00\x00\x00", b"\x00" * 8): raw = raw[8:] elif raw[:8].rstrip(b"\x00") == b"UNICODE": try: return raw[8:].decode("utf-16-be", "ignore").strip("\x00 ") or None except Exception: pass for enc in ("utf-8", "utf-16-le", "latin-1"): try: text = raw.decode(enc, "ignore").strip("\x00 ") if text: return text except Exception: continue return None text = str(value).strip() return text or None def gather(path: str) -> list[tuple[str, str]]: """Return an ordered list of (label, value) metadata strings.""" fields: list[tuple[str, str]] = [] fields.append(("name", os.path.basename(path))) img = None try: img = Image.open(path) except Exception: pass if img is not None: fields.append(("size", f"{img.width} x {img.height}")) fields.append(("format", img.format or "?")) fields.append(("color", _DEPTH.get(img.mode, img.mode))) dates: list[datetime.datetime] = [] comment = None if img is not None: try: exif = img.getexif() sub = {} try: sub = exif.get_ifd(_EXIF_IFD) except Exception: sub = {} for tag, src in ((_DATETIME, exif), (_DATETIME_ORIGINAL, sub), (_DATETIME_DIGITIZED, sub)): dt = _parse_exif_dt(src.get(tag)) if dt: dates.append(dt) comment = (_decode_comment(sub.get(_USER_COMMENT)) or _decode_comment(exif.get(_IMAGE_DESCRIPTION)) or _decode_comment(exif.get(_XP_COMMENT))) except Exception: pass if dates: fields.append(("exif date", min(dates).strftime("%Y-%m-%d %H:%M"))) try: st = os.stat(path) birth = getattr(st, "st_birthtime", None) if birth: fields.append(("file date", _fmt_ts(birth))) else: fields.append(("mod date", _fmt_ts(st.st_mtime))) except OSError: pass if comment: fields.append(("comment", comment)) fields.append(("c64 made", datetime.datetime.now().strftime("%Y-%m-%d %H:%M"))) fields.append(("system", f"{platform.system()} {platform.release()}")) distro = _linux_distro() if distro: fields.append(("distro", distro)) return fields def _linux_distro() -> str | None: """Linux distribution name + version from /etc/os-release, if available.""" try: rel = platform.freedesktop_os_release() # Python 3.10+ except (OSError, AttributeError): return None pretty = rel.get("PRETTY_NAME") if pretty: return pretty name = rel.get("NAME", "") version = rel.get("VERSION", rel.get("VERSION_ID", "")) return (f"{name} {version}".strip() or None)