142 lines
4.3 KiB
Python
142 lines
4.3 KiB
Python
"""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)
|