from datetime import datetime from typing import Mapping from app.schemas.document_verification import MetadataAnalysis KNOWN_EDITING_TOOLS = [ "Adobe Acrobat", "Adobe Photoshop", "Microsoft Word", "Canva", "WPS", "LibreOffice", "Preview", "Illustrator", "InDesign", "GIMP", "Scanner", "CamScanner", "Smallpdf", "iLovePDF", ] KNOWN_AI_TOOLS = [ "ChatGPT", "OpenAI", "DALL-E", "DALLĀ·E", "Midjourney", "Stable Diffusion", "ComfyUI", "Firefly", "Gemini", "Imagen", "Google AI", ] PDF_FIELD_MAP = { "creator": "creator", "producer": "producer", "author": "author", "title": "title", "subject": "subject", "keywords": "keywords", "creationDate": "creation_date", "creation_date": "creation_date", "modDate": "modification_date", "modification_date": "modification_date", } IMAGE_CAMERA_KEYS = {"make", "model", "cameramake", "cameramodel", "271", "272"} IMAGE_SOFTWARE_KEYS = {"software", "305"} IMAGE_DATE_KEYS = {"datetimeoriginal", "datetime original", "36867", "datetime", "306"} IMAGE_GPS_KEYS = {"gps", "gpsinfo", "34853"} class MetadataAnalyzer: def analyze_pdf_metadata(self, metadata: Mapping[str, str | None]) -> MetadataAnalysis: normalized = self._normalize_pdf_metadata(metadata) return self._build_result(normalized, metadata) def analyze_image_exif(self, exif: Mapping[str, str | None]) -> MetadataAnalysis: normalized = self._normalize_image_exif(exif) return self._build_result(normalized, exif) def _normalize_pdf_metadata(self, metadata: Mapping[str, str | None]) -> dict[str, str | None]: normalized: dict[str, str | None] = {} for source_key, target_key in PDF_FIELD_MAP.items(): value = metadata.get(source_key) if self._has_value(value): normalized[target_key] = str(value) return normalized def _normalize_image_exif(self, exif: Mapping[str, str | None]) -> dict[str, str | None]: normalized: dict[str, str | None] = {} for key, value in exif.items(): if not self._has_value(value): continue lowered_key = str(key).lower().replace("_", "").replace(" ", "") if lowered_key in IMAGE_CAMERA_KEYS: existing = normalized.get("camera") or "" normalized["camera"] = f"{existing} {value}".strip() elif lowered_key in IMAGE_SOFTWARE_KEYS: normalized["creator"] = str(value) elif lowered_key in IMAGE_DATE_KEYS: normalized["creation_date"] = str(value) elif lowered_key in IMAGE_GPS_KEYS or lowered_key.startswith("gps"): normalized["gps"] = str(value) else: normalized[str(key)] = str(value) return normalized def _build_result( self, normalized: Mapping[str, str | None], raw_metadata: Mapping[str, str | None], ) -> MetadataAnalysis: searchable_text = " ".join(str(value) for value in raw_metadata.values() if self._has_value(value)) metadata_found = any(self._has_value(value) for value in raw_metadata.values()) known_tools = self._detect_tools(searchable_text, KNOWN_EDITING_TOOLS) ai_tools = self._detect_tools(searchable_text, KNOWN_AI_TOOLS) modified_after_creation = self._modified_after_creation( normalized.get("creation_date"), normalized.get("modification_date"), ) camera_metadata_found = bool(normalized.get("camera")) or any( str(key).lower().replace("_", "").replace(" ", "") in IMAGE_CAMERA_KEYS for key, value in raw_metadata.items() if self._has_value(value) ) gps_found = bool(normalized.get("gps")) or any( str(key).lower().startswith("gps") or str(key) in IMAGE_GPS_KEYS for key, value in raw_metadata.items() if self._has_value(value) ) flags: list[str] = [] warnings: list[str] = [] risk = 0.0 if not metadata_found: warnings.append("No metadata found. This is a low-risk signal, not proof of authenticity.") if ai_tools: flags.append("ai_tool_metadata_detected") risk = max(risk, 0.85) if modified_after_creation: flags.append("modified_after_creation") risk = max(risk, 0.5) if known_tools: flags.append("editing_software_detected") risk = max(risk, 0.3) if camera_metadata_found: flags.append("camera_metadata_found") risk = max(0.0, risk - 0.1) if gps_found: flags.append("gps_metadata_found") return MetadataAnalysis( checked=True, metadata_found=metadata_found, creator=normalized.get("creator"), producer=normalized.get("producer"), author=normalized.get("author"), title=normalized.get("title"), subject=normalized.get("subject"), keywords=normalized.get("keywords"), creation_date=normalized.get("creation_date"), modification_date=normalized.get("modification_date"), modified_after_creation=modified_after_creation, editing_software_detected=bool(known_tools), known_tools_detected=known_tools, ai_tool_detected=bool(ai_tools), detected_ai_tools=ai_tools, camera_metadata_found=camera_metadata_found, gps_found=gps_found, metadata_risk_score=round(risk, 2), flags=flags, warnings=warnings, ) def _detect_tools(self, text: str, tools: list[str]) -> list[str]: lowered_text = text.lower() return [tool for tool in tools if tool.lower() in lowered_text] def _has_value(self, value: str | None) -> bool: if value is None: return False return str(value).strip().lower() not in {"", "none", "null"} def _modified_after_creation( self, creation_date: str | None, modification_date: str | None, ) -> bool: if not creation_date or not modification_date: return False creation = self._parse_metadata_date(creation_date) modification = self._parse_metadata_date(modification_date) if not creation or not modification: return creation_date != modification_date return modification > creation def _parse_metadata_date(self, value: str) -> datetime | None: cleaned = value.strip() if cleaned.startswith("D:"): cleaned = cleaned[2:] cleaned = cleaned.replace("Z", "") cleaned = cleaned.split("+", maxsplit=1)[0].split("-", maxsplit=1)[0] for fmt in ("%Y%m%d%H%M%S", "%Y%m%d%H%M", "%Y%m%d", "%Y:%m:%d %H:%M:%S"): try: return datetime.strptime(cleaned[: len(datetime.now().strftime(fmt))], fmt) except ValueError: continue return None