"""Tests for spruce.exif — mosaic EXIF injection.""" import shutil from pathlib import Path import piexif import pytest from piexif import ExifIFD, GPSIFD, ImageIFD from spruce.exif import USER_COMMENT_ASCII, write_mosaic_exif FIXTURES = Path(__file__).parent / "fixtures" BLANK_JPEG = FIXTURES / "blank.jpg" def _dms_pair_to_float(num: int, den: int) -> float: return num / den if den else 0.0 def _dms_to_deg( ref: bytes, dms: list[tuple[tuple[int, int], tuple[int, int], tuple[int, int]]] ) -> float: d0, m0, s0 = dms deg = ( _dms_pair_to_float(d0[0], d0[1]) + _dms_pair_to_float(m0[0], m0[1]) / 60.0 + _dms_pair_to_float(s0[0], s0[1]) / 3600.0 ) if ref in (b"S", b"W"): return -deg return deg @pytest.fixture def tmp_jpeg(tmp_path: Path) -> Path: dest = tmp_path / "mosaic.jpg" shutil.copy(BLANK_JPEG, dest) return dest @pytest.fixture def scan_meta() -> dict: return { "scan_id": 157743, "name": "Plot 7 AMR18 Full tube scan", "scan_time": "2024-06-28 11:00:00", "user": "Joanne", "nx": 103, "ny": 328, "dx": 3.01, "dy": 2.26, "end_x": 310.0, "end_y": 740.0, } @pytest.fixture def machine() -> dict: return {"label": "BW1-7 [AMR-18]", "version": "3.0.0.18", "machine_id": "7"} def test_write_mosaic_exif_round_trip(tmp_jpeg: Path, scan_meta: dict, machine: dict): assert write_mosaic_exif( tmp_jpeg, scan_meta, machine, 157743, None, ) exif = piexif.load(str(tmp_jpeg)) assert exif["Exif"][ExifIFD.DateTimeOriginal] == b"2024:06:28 11:00:00" assert ( exif["0th"][ImageIFD.ImageDescription] == b"BW1-7 [AMR-18] scan 157743 (Plot 7 AMR18 Full tube scan)" ) assert exif["0th"][ImageIFD.Artist] == b"Joanne" assert exif["0th"][ImageIFD.Software] == b"RootView 3.0.0.18" assert exif["0th"][ImageIFD.ProcessingSoftware] == b"spruce-scraper/1.0" uc = exif["Exif"][ExifIFD.UserComment] assert uc.startswith(USER_COMMENT_ASCII) tail = uc[len(USER_COMMENT_ASCII) :] assert b"SPRUCE scan 157743" in tail assert b"103x328" in tail assert b"see metadata.json" in tail assert b"plot_number" not in tail assert ImageIFD.XPKeywords not in exif["0th"] assert "GPS" not in exif or not exif["GPS"] def test_gps_decode(tmp_jpeg: Path, scan_meta: dict, machine: dict): mmeta = { "latitude_wgs_84": 47.5047, "longitude_wgs_84": -93.4530, "elevation_masl": 418.0, } assert write_mosaic_exif(tmp_jpeg, scan_meta, machine, 157743, mmeta) exif = piexif.load(str(tmp_jpeg)) gps = exif["GPS"] lat = _dms_to_deg(gps[GPSIFD.GPSLatitudeRef], gps[GPSIFD.GPSLatitude]) lon = _dms_to_deg(gps[GPSIFD.GPSLongitudeRef], gps[GPSIFD.GPSLongitude]) assert abs(lat - 47.5047) < 1e-5 assert abs(lon - (-93.4530)) < 1e-5 alt = gps[GPSIFD.GPSAltitude] assert alt[0] / alt[1] == pytest.approx(418.0) def test_no_gps_no_keywords_when_meta_none( tmp_jpeg: Path, scan_meta: dict, machine: dict ): assert write_mosaic_exif(tmp_jpeg, scan_meta, machine, 157743, None) exif = piexif.load(str(tmp_jpeg)) assert ImageIFD.XPKeywords not in exif["0th"] assert "GPS" not in exif or not exif["GPS"] def test_xp_keywords_treatment(tmp_jpeg: Path, scan_meta: dict, machine: dict): mmeta = { "plot_number": 7, "enclosure": False, "temp_treatment": 2.25, "co2_treatment": "ambient", } assert write_mosaic_exif(tmp_jpeg, scan_meta, machine, 157743, mmeta) exif = piexif.load(str(tmp_jpeg)) raw = exif["0th"][ImageIFD.XPKeywords] text = bytes(raw).decode("utf-16le").rstrip("\x00") assert "SPRUCE" in text assert "plot 7" in text assert "no enclosure" in text assert "temp" in text assert "2.25" in text assert "aCO2" in text uc = exif["Exif"][ExifIFD.UserComment] uct = uc[len(USER_COMMENT_ASCII) :].decode("ascii", errors="replace") assert "plot_number 7" in uct assert "enclosure no" in uct assert "temp_treatment 2.25" in uct assert "co2_treatment ambient" in uct