Files
remove-ai-watermarks/src/remove_ai_watermarks/noai/extractor.py
T
Victor KuznetsovandClaude Opus 4.7 888c8c2556 chore(types): clear strict-pyright debt across src (0 errors)
Make `pyright src/` strict-clean via a hybrid: pure-logic files are fully typed
(piexif gets a local typings/ stub; PIL info-dict loops guard isinstance(key, str);
progress returns Callable[..., None]; availability checks use importlib.util.find_spec
instead of unused imports), while the irreducibly-untyped cv2/torch/diffusers boundary
files carry a documented per-file `# pyright:` relax pragma (or a ctrlregen
executionEnvironment) that disables only the unknown-type rules. Public ndarray-returning
signatures on the relaxed engines are annotated NDArray[Any] so strict consumers (cli.py)
stay clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 14:00:15 -07:00

156 lines
4.5 KiB
Python

"""Read-only metadata extraction from PNG and JPEG images.
Provides functions to pull all metadata, AI-only metadata, or a
human-readable summary without modifying the source file.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, cast
if TYPE_CHECKING:
from pathlib import Path
import piexif
from PIL import Image
from remove_ai_watermarks.noai.c2pa import extract_c2pa_chunk, extract_c2pa_info, has_c2pa_metadata
from remove_ai_watermarks.noai.constants import AI_KEYWORDS, AI_METADATA_KEYS, PNG_METADATA_KEYS
def extract_metadata(source_path: Path) -> dict[str, Any]:
"""
Extract all metadata from a PNG or JPG file.
Args:
source_path: Path to the source image file.
Returns:
Dictionary containing all extracted metadata.
"""
metadata: dict[str, Any] = {}
with Image.open(source_path) as img:
# Extract EXIF data
if "exif" in img.info:
try:
exif_dict = piexif.load(img.info["exif"])
metadata["exif"] = exif_dict
except Exception:
metadata["exif_raw"] = img.info["exif"]
# Extract standard PNG metadata
for key in PNG_METADATA_KEYS:
if key in img.info:
metadata[key] = img.info[key]
# Extract all other metadata including AI-specific
for key, value in img.info.items():
if not isinstance(key, str):
continue
if key not in metadata and key not in ["exif"]:
metadata[key] = value
# Extract DPI and gamma if present
if "dpi" in img.info:
metadata["dpi"] = img.info["dpi"]
if "gamma" in img.info:
metadata["gamma"] = img.info["gamma"]
# Check for C2PA metadata
if has_c2pa_metadata(source_path):
metadata["c2pa"] = extract_c2pa_info(source_path)
c2pa_chunk = extract_c2pa_chunk(source_path)
if c2pa_chunk:
metadata["c2pa_chunk"] = c2pa_chunk
return metadata
def extract_ai_metadata(source_path: Path) -> dict[str, Any]:
"""
Extract only AI-generated metadata from a PNG or JPG file.
Args:
source_path: Path to the source image file.
Returns:
Dictionary containing only AI-related metadata.
"""
ai_metadata: dict[str, Any] = {}
with Image.open(source_path) as img:
for key in AI_METADATA_KEYS:
if key in img.info:
ai_metadata[key] = img.info[key]
for key, value in img.info.items():
if not isinstance(key, str):
continue
key_lower = key.lower()
if key not in ai_metadata and any(kw in key_lower for kw in AI_KEYWORDS):
ai_metadata[key] = value
# Check for C2PA metadata
if has_c2pa_metadata(source_path):
ai_metadata["c2pa"] = extract_c2pa_info(source_path)
c2pa_chunk = extract_c2pa_chunk(source_path)
if c2pa_chunk:
ai_metadata["c2pa_chunk"] = c2pa_chunk
return ai_metadata
def has_ai_metadata(image_path: Path) -> bool:
"""
Check if an image contains AI-generated metadata.
Args:
image_path: Path to the image file.
Returns:
True if AI metadata is detected, False otherwise.
"""
with Image.open(image_path) as img:
for key in AI_METADATA_KEYS:
if key in img.info:
return True
return bool(has_c2pa_metadata(image_path))
def get_ai_metadata_summary(source_path: Path) -> str:
"""
Get a human-readable summary of AI metadata.
Args:
source_path: Path to the source image file.
Returns:
Formatted string with AI metadata summary.
"""
ai_meta = extract_ai_metadata(source_path)
if not ai_meta:
return "No AI metadata found."
lines = ["AI Image Metadata:"]
lines.append("-" * 40)
for key, value in ai_meta.items():
if key == "c2pa_chunk":
continue
if key == "c2pa" and isinstance(value, dict):
lines.append("C2PA Metadata:")
for ck, cv in cast("dict[str, Any]", value).items():
lines.append(f" {ck}: {cv}")
elif isinstance(value, str) and len(value) > 100:
value = value[:100] + "..."
lines.append(f"{key}: {value}")
elif isinstance(value, bytes):
lines.append(f"{key}: <binary data ({len(value)} bytes)>")
else:
lines.append(f"{key}: {value}")
return "\n".join(lines)