mirror of
https://github.com/guillaumemeyer/watermarks-remover.git
synced 2026-08-22 13:11:57 +02:00
fix: detect AI markers in compressed PNG text (#127)
Co-authored-by: Guillaume Meyer (The Opinionated Man) <1385518+guillaumemeyer@users.noreply.github.com>
This commit is contained in:
co-authored by
Guillaume Meyer
parent
e2170a8e17
commit
360c8e8e4d
@@ -295,16 +295,23 @@ def _generator_product_hits(entries: list[tuple[str, str]]) -> list[str]:
|
||||
return hits
|
||||
|
||||
|
||||
def _png_text_hits(payload: bytes, ctype: bytes) -> tuple[list[str], list[str]]:
|
||||
"""Return flat-marker and product-name hits from a PNG text chunk."""
|
||||
entries = _png_text_entries(payload, ctype)
|
||||
decoded = "\n".join(value for _key, value in entries).encode("utf-8")
|
||||
hits = _contains_any(payload + b"\n" + decoded, AI_META_HINTS + C2PA_MARKERS)
|
||||
return hits, _generator_product_hits(entries)
|
||||
|
||||
|
||||
def _text_chunk_is_ai(payload: bytes, ctype: bytes) -> bool:
|
||||
"""True when a PNG text chunk carries AI/C2PA markers.
|
||||
|
||||
Flat markers (AI_META_HINTS + C2PA_MARKERS) match anywhere in the
|
||||
payload; generator product names only match generator-bearing key
|
||||
Flat markers (AI_META_HINTS + C2PA_MARKERS) match the raw payload and
|
||||
decoded text; generator product names only match generator-bearing key
|
||||
values (see _generator_product_hits).
|
||||
"""
|
||||
if _contains_any(payload, AI_META_HINTS + C2PA_MARKERS):
|
||||
return True
|
||||
return bool(_generator_product_hits(_png_text_entries(payload, ctype)))
|
||||
hits, product_hits = _png_text_hits(payload, ctype)
|
||||
return bool(hits or product_hits)
|
||||
|
||||
|
||||
def inspect_png(data: bytes) -> tuple[bool, bool, list[str]]:
|
||||
@@ -329,12 +336,11 @@ def inspect_png(data: bytes) -> tuple[bool, bool, list[str]]:
|
||||
has_c2pa = True
|
||||
findings.append(f"PNG chunk {name} (possible C2PA container)")
|
||||
if ctype in (b"tEXt", b"zTXt", b"iTXt", b"eXIf"):
|
||||
hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
|
||||
product_hits = (
|
||||
_generator_product_hits(_png_text_entries(payload, ctype))
|
||||
if ctype in (b"tEXt", b"zTXt", b"iTXt")
|
||||
else []
|
||||
)
|
||||
if ctype in (b"tEXt", b"zTXt", b"iTXt"):
|
||||
hits, product_hits = _png_text_hits(payload, ctype)
|
||||
else:
|
||||
hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
|
||||
product_hits = []
|
||||
if hits or product_hits:
|
||||
has_ai = True
|
||||
if any(h.lower() in ("c2pa", "contentcredentials", "jumb") for h in hits):
|
||||
|
||||
@@ -137,6 +137,27 @@ def test_itext_software_matches():
|
||||
assert any("AI generator" in f for f in findings)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("ctype", "payload"),
|
||||
[
|
||||
(b"zTXt", b"Comment\x00\x00" + zlib.compress(b"Generated by AI")),
|
||||
(
|
||||
b"iTXt",
|
||||
b"Comment\x00\x01\x00\x00\x00" + zlib.compress(b"Generated by AI"),
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_compressed_text_flat_hint_matches_and_is_dropped(ctype: bytes, payload: bytes):
|
||||
data = _minimal_png_with_text_chunk(ctype, payload)
|
||||
_has_c2pa, has_ai, findings = inspect_png(data)
|
||||
assert has_ai is True
|
||||
assert any("Generated by" in f for f in findings)
|
||||
|
||||
cleaned, actions = strip_png(data, strip_all_text=False)
|
||||
assert ctype not in cleaned
|
||||
assert any("drop" in action for action in actions)
|
||||
|
||||
|
||||
def test_strip_keep_mode_drops_generator_tagged_chunk():
|
||||
data = _minimal_png_with_text_chunk(b"tEXt", _text("Software", "ChatGPT"))
|
||||
cleaned, actions = strip_png(data, strip_all_text=False)
|
||||
|
||||
Reference in New Issue
Block a user