mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-07 14:38:35 +02:00
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>
156 lines
4.5 KiB
Python
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)
|