mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-07-31 19:47:24 +02:00
fix(identify): attribute C2PA by claim_generator, not incidental issuer tokens (v0.6.1)
Verified on real signed files that the issuer byte-scan mis-attributes multi-entity manifests: Leica read as "Truepic" (timestamp authority in the chain), Nikon as "Adobe Firefly" (XMP-toolkit "Adobe" + the sample's "Adobe_MAX" name), Truepic as "Google". Platform attribution now prefers the claim generator (what produced the asset) and falls back to the issuer scan. - New _CLAIM_GENERATOR_PLATFORM map + _platform_from_generator; claim generator read for non-PNG via the now-public c2pa.cbor_text_after. - Device tokens listed only where verified against a real C2PA file (Leica lc_c2pa, Nikon, Truepic Lens); Pixel/Samsung/Sony/Canon/Bria deferred until a real sample confirms the in-manifest string. Camera C2PA marks capture authenticity, so these never set is_ai. - cbor_text_after made public (was _cbor_text_after); call sites + tests updated. - Regression test: claim_generator beats incidental Adobe/Google/Truepic tokens. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
2676325184
commit
dda2ee7fbb
@@ -337,6 +337,25 @@ class TestIdentifyIptcAi:
|
||||
assert "Gemini" in r.platform
|
||||
|
||||
|
||||
class TestIdentifyC2paClaimGenerator:
|
||||
"""C2PA attribution prefers claim_generator over incidental issuer tokens."""
|
||||
|
||||
def test_claim_generator_beats_incidental_tokens(self, tmp_path: Path):
|
||||
# Real manifests mention timestamp authorities / XMP toolkits, so
|
||||
# "Adobe"/"Google"/"Truepic" appear incidentally; the claim generator
|
||||
# (a Leica camera) must win the platform attribution. Regression guard
|
||||
# for the real-sample mis-attribution (Leica->Truepic, Nikon->Adobe).
|
||||
gen = b"M11-P/2.0.1 lc_c2pa"
|
||||
blob = (
|
||||
b"\xff\xd8\xff\xe1 c2pa.claim jumbf Adobe Google Truepic "
|
||||
b"claim_generator" + bytes([0x60 + len(gen)]) + gen + b" \xff\xd9"
|
||||
)
|
||||
p = tmp_path / "leica_like.jpg"
|
||||
p.write_bytes(blob)
|
||||
r = identify(p, check_visible=False, check_invisible=False)
|
||||
assert r.platform == "Leica (camera, C2PA capture)"
|
||||
|
||||
|
||||
# ── Open invisible watermark (SD/SDXL/FLUX) integration ─────────────
|
||||
|
||||
from remove_ai_watermarks.invisible_watermark import is_available as _wm_available # noqa: E402
|
||||
|
||||
+9
-9
@@ -8,8 +8,8 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from remove_ai_watermarks.noai.c2pa import (
|
||||
_cbor_text_after,
|
||||
_parse_c2pa_chunk,
|
||||
cbor_text_after,
|
||||
extract_c2pa_chunk,
|
||||
extract_c2pa_info,
|
||||
has_c2pa_metadata,
|
||||
@@ -193,36 +193,36 @@ class TestC2PAInjectValidation:
|
||||
|
||||
|
||||
class TestCborTextAfter:
|
||||
"""_cbor_text_after handles the three CBOR text-string length prefixes."""
|
||||
"""cbor_text_after handles the three CBOR text-string length prefixes."""
|
||||
|
||||
def test_direct_length(self):
|
||||
# major-type 3, direct length (0x60 + len). "abc" -> 0x63.
|
||||
payload = b"name" + bytes([0x63]) + b"abc"
|
||||
assert _cbor_text_after(payload, b"name") == "abc"
|
||||
assert cbor_text_after(payload, b"name") == "abc"
|
||||
|
||||
def test_one_byte_length(self):
|
||||
s = b"x" * 30
|
||||
payload = b"name" + bytes([0x78, 30]) + s
|
||||
assert _cbor_text_after(payload, b"name") == "x" * 30
|
||||
assert cbor_text_after(payload, b"name") == "x" * 30
|
||||
|
||||
def test_two_byte_length(self):
|
||||
s = b"y" * 300
|
||||
payload = b"name" + bytes([0x79]) + struct.pack(">H", 300) + s
|
||||
assert _cbor_text_after(payload, b"name") == "y" * 300
|
||||
assert cbor_text_after(payload, b"name") == "y" * 300
|
||||
|
||||
def test_key_not_found_returns_none(self):
|
||||
assert _cbor_text_after(b"nothing here", b"name") is None
|
||||
assert cbor_text_after(b"nothing here", b"name") is None
|
||||
|
||||
def test_key_at_end_returns_none(self):
|
||||
assert _cbor_text_after(b"prefixname", b"name") is None
|
||||
assert cbor_text_after(b"prefixname", b"name") is None
|
||||
|
||||
def test_invalid_head_returns_none(self):
|
||||
# 0x00 is not a text-string head.
|
||||
assert _cbor_text_after(b"name" + bytes([0x00]) + b"abc", b"name") is None
|
||||
assert cbor_text_after(b"name" + bytes([0x00]) + b"abc", b"name") is None
|
||||
|
||||
def test_latin1_fallback_on_invalid_utf8(self):
|
||||
payload = b"name" + bytes([0x61]) + b"\xff" # len 1, invalid utf-8
|
||||
assert _cbor_text_after(payload, b"name") is not None
|
||||
assert cbor_text_after(payload, b"name") is not None
|
||||
|
||||
|
||||
class TestSynthIDVerdict:
|
||||
|
||||
Reference in New Issue
Block a user