feat(metadata): blank AI-generator tokens in AVIF/HEIF Exif meta-box items

Closes a documented coverage gap (P2#9): an AI Software/Make/Artist/ImageDescription
token in an EXIF item (its TIFF bytes live in mdat/idat) survived remove_ai_metadata
because the top-level box stripper and (absent pillow-heif) the PIL EXIF reader can't
reach it. New isobmff.blank_ai_exif_tokens finds EXIF TIFF blocks by their II/MM
byte-order header, validates each with piexif (a coincidental II/MM run in pixels
won't parse as a TIFF IFD, so it's ignored), and overwrites any AI_GENERATOR_TOKENS-
bearing value with same-length spaces -- so box sizes and iloc offsets stay valid and
the coded image is untouched (mirrors blank_ai_xmp_packets; no iinf/iloc surgery, no
exiftool dep). Camera/editor EXIF without an AI token is preserved. Wired into
remove_ai_metadata's ISOBMFF path. Covers the realistic AI-generator-token case; xAI-
signature-in-meta-box-EXIF (Grok is JPEG-only) stays out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Victor Kuznetsov
2026-06-19 10:43:35 -07:00
parent 3f5d6a0af1
commit d5845a72f3
6 changed files with 141 additions and 12 deletions
+19
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import shutil
import struct
import subprocess
from pathlib import Path
@@ -125,6 +126,24 @@ class TestHasAiMetadata:
assert not has_ai_metadata(out)
def test_remove_ai_metadata_blanks_exif_token_item_in_avif(self, tmp_path: Path):
"""End-to-end: ``remove_ai_metadata`` blanks an AI-generator EXIF token
stored as a meta-box Exif item (bytes in mdat) without re-encoding."""
from remove_ai_watermarks.metadata import remove_ai_metadata
ftyp = b"\x00\x00\x00\x18ftypavif\x00\x00\x00\x00avifmif1"
blob = piexif.dump({"0th": {piexif.ImageIFD.Software: b"Midjourney", piexif.ImageIFD.Make: b"NIKON"}})
mdat = struct.pack(">I", 8 + len(blob)) + b"mdat" + blob
src = tmp_path / "in.avif"
src.write_bytes(ftyp + mdat)
out = tmp_path / "out.avif"
remove_ai_metadata(src, out)
cleaned = out.read_bytes()
assert len(cleaned) == len(ftyp + mdat) # in place, no re-encode
assert b"Midjourney" not in cleaned # AI token gone
assert b"NIKON" in cleaned # camera tag preserved
def test_detects_iptc_trained_algorithmic_media_marker(self, tmp_path: Path):
"""Some pipelines embed only the IPTC AI marker in XMP, no C2PA manifest."""
path = tmp_path / "fake.jpg"
+39
View File
@@ -36,6 +36,7 @@ from remove_ai_watermarks.noai.extractor import (
has_ai_metadata,
)
from remove_ai_watermarks.noai.isobmff import (
blank_ai_exif_tokens,
is_isobmff,
strip_c2pa_boxes,
)
@@ -366,6 +367,44 @@ class TestISOBMFF:
assert stripped == 0
assert cleaned == data
@staticmethod
def _avif_with_exif(exif_0th: dict) -> bytes:
"""A fake AVIF (ftyp + mdat) whose mdat carries an EXIF TIFF block, as a
HEIF/AVIF ``Exif`` meta-box item stores it (bytes in mdat)."""
import piexif
blob = piexif.dump({"0th": exif_0th})
mdat = struct.pack(">I", 8 + len(blob)) + b"mdat" + blob
return FTYP + mdat
def test_blank_ai_token_in_exif_item(self):
import piexif
data = self._avif_with_exif({piexif.ImageIFD.Software: b"DALL-E", piexif.ImageIFD.Make: b"Canon"})
out, blanked = blank_ai_exif_tokens(data)
assert blanked == 1
assert len(out) == len(data) # same length -> box sizes / iloc stay valid
assert b"DALL-E" not in out # AI token destroyed
assert b"Canon" in out # camera tag preserved
# The TIFF structure still parses, with the AI value blanked and Make kept.
blob = out[out.index(b"Exif\x00\x00") + 6 :]
ifd = piexif.load(blob)["0th"]
assert ifd[piexif.ImageIFD.Software].strip() == b""
assert ifd[piexif.ImageIFD.Make] == b"Canon"
def test_blank_leaves_clean_exif_untouched(self):
import piexif
data = self._avif_with_exif({piexif.ImageIFD.Software: b"Adobe Photoshop", piexif.ImageIFD.Make: b"NIKON"})
out, blanked = blank_ai_exif_tokens(data)
assert blanked == 0
assert out == data # no AI token -> byte-for-byte unchanged
def test_blank_no_exif_is_noop(self):
out, blanked = blank_ai_exif_tokens(FTYP + b"\x00\x00\x00\x0cmdat" + b"pixels!!")
assert blanked == 0
assert out == FTYP + b"\x00\x00\x00\x0cmdat" + b"pixels!!"
class TestC2PAInvalidSignature:
"""A .png file that is not actually PNG-signed must read as clean, not crash."""