mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-06 22:18:36 +02:00
Recover malformed metadata containers, release 0.20.2
This commit is contained in:
@@ -22,6 +22,7 @@ from remove_ai_watermarks.metadata import (
|
||||
iptc_ai_system,
|
||||
remove_ai_metadata,
|
||||
samsung_genai,
|
||||
strip_and_verify,
|
||||
synthid_source,
|
||||
xai_signature,
|
||||
)
|
||||
@@ -29,6 +30,28 @@ from remove_ai_watermarks.metadata import (
|
||||
# Real, committed C2PA sample images used to ground the SynthID-source tests.
|
||||
SAMPLES_DIR = Path(__file__).resolve().parent.parent / "data" / "fixtures" / "provenance"
|
||||
|
||||
|
||||
def _png_chunk(kind: bytes, payload: bytes, *, corrupt_crc: bool = False) -> bytes:
|
||||
"""Encode one PNG chunk, optionally corrupting its CRC."""
|
||||
import zlib
|
||||
|
||||
crc = zlib.crc32(kind + payload) & 0xFFFFFFFF
|
||||
if corrupt_crc:
|
||||
crc ^= 1
|
||||
return struct.pack(">I", len(payload)) + kind + payload + struct.pack(">I", crc)
|
||||
|
||||
|
||||
def _corrupt_c2pa_png(path: Path) -> Path:
|
||||
"""Write a synthetic PNG that Pillow rejects but OpenCV can decode."""
|
||||
Image.new("RGB", (32, 32), (80, 120, 160)).save(path)
|
||||
png = path.read_bytes()
|
||||
idat_start = png.index(b"IDAT") - 4
|
||||
fake_c2pa = _png_chunk(b"caBX", b"\x00\x00\x00\x10jumbsynthetic-c2pa")
|
||||
bad_exif = _png_chunk(b"eXIf", b"Exif\x00\x00synthetic", corrupt_crc=True)
|
||||
path.write_bytes(png[:idat_start] + fake_c2pa + bad_exif + png[idat_start:])
|
||||
return path
|
||||
|
||||
|
||||
# ── Key detection ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -276,6 +299,39 @@ class TestHasAiMetadata:
|
||||
assert result == out
|
||||
assert out.read_bytes() == truncated.read_bytes()
|
||||
|
||||
def test_strip_and_verify_normalizes_decodable_copy_through(self, tmp_path: Path):
|
||||
"""A decodable raster must not retain C2PA after verified stripping."""
|
||||
import numpy as np
|
||||
|
||||
from remove_ai_watermarks import image_io
|
||||
|
||||
src = _corrupt_c2pa_png(tmp_path / "source.png")
|
||||
out = tmp_path / "clean.png"
|
||||
assert has_ai_metadata(src)
|
||||
before = image_io.imread(src)
|
||||
assert before is not None
|
||||
|
||||
result, remaining = strip_and_verify(src, out)
|
||||
|
||||
assert result == out
|
||||
assert remaining == {}
|
||||
assert not has_ai_metadata(out)
|
||||
after = image_io.imread(out)
|
||||
assert after is not None
|
||||
assert np.array_equal(after, before)
|
||||
|
||||
def test_strip_and_verify_reports_markers_in_undecodable_copy_through(self, tmp_path: Path):
|
||||
"""A truly undecodable file keeps the established fail-safe result."""
|
||||
src = _corrupt_c2pa_png(tmp_path / "source.png")
|
||||
src.write_bytes(src.read_bytes()[: src.stat().st_size // 2])
|
||||
out = tmp_path / "copy.png"
|
||||
|
||||
result, remaining = strip_and_verify(src, out)
|
||||
|
||||
assert result == out
|
||||
assert "c2pa_manifest" in remaining
|
||||
assert out.read_bytes() == src.read_bytes()
|
||||
|
||||
|
||||
class TestC2paMarkerIn:
|
||||
"""The C2PA presence check requires a JUMBF wrapper or the C2PA uuid box, so
|
||||
|
||||
Reference in New Issue
Block a user