diff --git a/src/remove_ai_watermarks/metadata.py b/src/remove_ai_watermarks/metadata.py index 1b66c52..ebd6e6d 100644 --- a/src/remove_ai_watermarks/metadata.py +++ b/src/remove_ai_watermarks/metadata.py @@ -198,15 +198,21 @@ def get_ai_metadata(image_path: Path) -> dict[str, str]: result: dict[str, str] = {} - with Image.open(image_path) as img: - for key, value in img.info.items(): - if _is_ai_key(key): - if isinstance(value, bytes): - result[key] = f"" - elif isinstance(value, str) and len(value) > 200: - result[key] = value[:200] + "…" - else: - result[key] = str(value) + # PIL may not open AVIF/HEIF/JPEG-XL without optional plugins (and + # ultralytics' Image.open patch can raise ModuleNotFoundError); fall through + # to the C2PA/binary path on any open failure. See CLAUDE.md. + try: + with Image.open(image_path) as img: + for key, value in img.info.items(): + if _is_ai_key(key): + if isinstance(value, bytes): + result[key] = f"" + elif isinstance(value, str) and len(value) > 200: + result[key] = value[:200] + "…" + else: + result[key] = str(value) + except Exception as exc: + logger.debug("PIL could not open %s for AI-metadata scan: %s", image_path, exc) # C2PA manifest fields from the single canonical parser (noai/c2pa.py). c2pa = extract_c2pa_info(image_path) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index a5a97c1..43f7da3 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -159,6 +159,13 @@ class TestGetAiMetadata: assert meta["parameters"].endswith("…") assert len(meta["parameters"]) <= 205 + def test_unopenable_file_does_not_raise(self, tmp_path: Path): + # PIL can't open HEIC without pillow-heif; get_ai_metadata must fall + # through to the binary scan, not propagate UnidentifiedImageError. + path = tmp_path / "iphone.heic" + path.write_bytes(b"\x00\x00\x00\x18ftypheic" + b"\x00" * 64) + assert get_ai_metadata(path) == {} + @pytest.mark.skipif(not SAMPLES_DIR.exists(), reason="data/samples not present") class TestGetAiMetadataRealSample: