From 360c8e8e4d4ffb405d8a90d838917a3558207c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=9D=E5=BF=83Yearth?= Date: Wed, 19 Aug 2026 02:56:48 +0800 Subject: [PATCH] fix: detect AI markers in compressed PNG text (#127) Co-authored-by: Guillaume Meyer (The Opinionated Man) <1385518+guillaumemeyer@users.noreply.github.com> --- service/scripts/image_meta.py | 28 +++++++++++++++++----------- tests/test_ai_generator_hints.py | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/service/scripts/image_meta.py b/service/scripts/image_meta.py index 70892df..b79a70c 100755 --- a/service/scripts/image_meta.py +++ b/service/scripts/image_meta.py @@ -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): diff --git a/tests/test_ai_generator_hints.py b/tests/test_ai_generator_hints.py index d93374a..7fde675 100644 --- a/tests/test_ai_generator_hints.py +++ b/tests/test_ai_generator_hints.py @@ -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)