mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-07-09 09:57:48 +02:00
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:
@@ -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"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user