88 lines
3.2 KiB
Python
88 lines
3.2 KiB
Python
"""Characterize EXIF behavior (donor: build_exif_caption_from_result,
|
|
read_existing_exif, write_exif merge/idempotency, _rec_has_nsfw,
|
|
filter_nsfw_tagged). Uses the real exiftool on synthetic temp files."""
|
|
import subprocess
|
|
|
|
import pytest
|
|
|
|
import photo_analyzer as pa
|
|
from conftest import EXIFTOOL
|
|
|
|
needs_exiftool = pytest.mark.skipif(not EXIFTOOL, reason="exiftool not on PATH")
|
|
|
|
RESULT = {
|
|
"description": "A dog on a beach.",
|
|
"tags": ["dog", "beach"],
|
|
"mood": "joyful",
|
|
"location_hint": "Rome, Italy",
|
|
"approx_year": 2015,
|
|
}
|
|
|
|
|
|
def test_caption_golden_full():
|
|
assert pa.build_exif_caption_from_result(RESULT) == (
|
|
"A dog on a beach. | Tags: dog, beach | Mood: joyful | "
|
|
"Location: Rome, Italy | ~2015")
|
|
|
|
|
|
def test_caption_golden_sparse():
|
|
assert pa.build_exif_caption_from_result(
|
|
{"description": "X.", "tags": [], "location_hint": None}) == "X."
|
|
|
|
|
|
def test_rec_has_nsfw_list_scalar_case():
|
|
assert pa._rec_has_nsfw({"Keywords": ["beach", "NSFW "]})
|
|
assert pa._rec_has_nsfw({"Subject": "nsfw"})
|
|
assert not pa._rec_has_nsfw({"Keywords": ["sfw"], "Subject": ["beach"]})
|
|
assert not pa._rec_has_nsfw({})
|
|
|
|
|
|
@needs_exiftool
|
|
def test_write_exif_roundtrip_and_idempotent(img):
|
|
caption = pa.build_exif_caption_from_result(RESULT)
|
|
assert pa.write_exif(str(img), caption, RESULT["tags"])
|
|
rec = pa.read_existing_exif(str(img))
|
|
assert rec["ImageDescription"] == caption
|
|
assert rec["XPComment"] == caption
|
|
assert rec["Subject"] == RESULT["tags"]
|
|
assert rec["Keywords"] == RESULT["tags"]
|
|
|
|
# second write with the same caption must not duplicate anything
|
|
assert pa.write_exif(str(img), caption, RESULT["tags"])
|
|
rec2 = pa.read_existing_exif(str(img))
|
|
assert rec2["ImageDescription"] == caption
|
|
assert rec2["Keywords"] == RESULT["tags"]
|
|
|
|
|
|
@needs_exiftool
|
|
def test_write_exif_preserves_existing_metadata(img):
|
|
subprocess.run(
|
|
["exiftool", "-m", "-overwrite_original", "-Artist=Original Artist",
|
|
"-ImageDescription=User caption", "-Keywords=userkw", str(img)],
|
|
check=True, capture_output=True)
|
|
assert pa.write_exif(str(img), "AI caption", ["aitag"])
|
|
rec = pa.read_existing_exif(str(img))
|
|
# donor behavior: AI caption prepended, user caption kept after ' | '
|
|
assert rec["ImageDescription"] == "AI caption | User caption"
|
|
kws = rec["Keywords"] if isinstance(rec["Keywords"], list) else [rec["Keywords"]]
|
|
assert "userkw" in kws and "aitag" in kws, "keywords merge, never replace"
|
|
full = subprocess.run(["exiftool", "-j", "-Artist", str(img)],
|
|
capture_output=True, text=True)
|
|
assert "Original Artist" in full.stdout, "non-owned field preserved"
|
|
|
|
|
|
@needs_exiftool
|
|
def test_filter_nsfw_tagged_splits_and_normalizes(img, tmp_path):
|
|
from conftest import write_fixture
|
|
clean = write_fixture("fx-blocks-02", tmp_path / "clean.jpg")
|
|
subprocess.run(["exiftool", "-m", "-overwrite_original",
|
|
"-Keywords=nsfw", "-Subject=nsfw", str(img)],
|
|
check=True, capture_output=True)
|
|
kept, skipped = pa.filter_nsfw_tagged([str(img), str(clean)])
|
|
assert kept == [str(clean)]
|
|
assert skipped == [str(img)]
|
|
|
|
|
|
def test_filter_nsfw_tagged_empty_list_noop():
|
|
assert pa.filter_nsfw_tagged([]) == ([], [])
|