Add EXIF writing and machine metadata support
This commit is contained in:
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 631 B |
@@ -0,0 +1,138 @@
|
||||
"""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
|
||||
@@ -143,7 +143,11 @@ def test_recheck_archive_skips_mosaic_urls(tmp_path):
|
||||
mosaic_url = "http://192.0.2.1:8011/RootView_Database/158374/mosaic.jpg"
|
||||
p.mark_done(mosaic_url)
|
||||
p.save()
|
||||
# recheck_verifies a non-zero mosaic exists under */*/<scan_id>/mosaic.jpg
|
||||
mpath = tmp_path / "M" / "2020-01-01" / "158374" / "mosaic.jpg"
|
||||
mpath.parent.mkdir(parents=True)
|
||||
mpath.write_bytes(b"\xff\xd8\xff\xd9") # minimal JPEG soff + eoi
|
||||
|
||||
removed = recheck_archive(tmp_path, p)
|
||||
assert removed == 0
|
||||
assert p.is_done(mosaic_url) # mosaics are never touched
|
||||
assert p.is_done(mosaic_url)
|
||||
|
||||
@@ -60,6 +60,8 @@ def test_load_config_defaults(tmp_path):
|
||||
assert cfg["timeout"] == 60
|
||||
assert cfg["request_delay"] == 0.5
|
||||
assert cfg["output_dir"] == "archives"
|
||||
assert cfg["write_exif"] is True
|
||||
assert cfg["machine_metadata"] == {}
|
||||
|
||||
|
||||
def test_load_config_overrides(tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user