""" Stage 2.5 - Metadata Forensic Analysis. Hardened version: downweighted to minor corroborating signal only. Missing metadata is NOT evidence of manipulation. """ import io from typing import Dict, Any, List, Optional from PIL import Image import piexif def extract_metadata(image_bytes: bytes) -> Dict[str, Any]: """ Extract EXIF metadata and analyze for forensic signals. IMPORTANT: Metadata is a MINOR CORROBORATING SIGNAL ONLY. - Absence of EXIF is common (social media, screenshots, editing) - Missing metadata does NOT push verdict toward FAKE - Software signatures are informative but not definitive """ try: image = Image.open(io.BytesIO(image_bytes)) image.load() # Force load to access EXIF except Exception as e: return _empty_metadata(f"Failed to open image: {e}") # Extract raw EXIF exif_data = _extract_exif(image) # Analyze signals has_exif = len(exif_data) > 0 camera_model = _get_camera_model(exif_data) software = _get_software(exif_data) gps_info = _get_gps(exif_data) dimensions = f"{image.width}x{image.height}" is_standard_square = _is_standard_square(image.width, image.height) # Build suspicious flags (INFORMATIVE ONLY - weight: minor) suspicious_flags = [] if not has_exif: suspicious_flags.append({ "flag": "NO_EXIF", "description": "No EXIF metadata found", "weight": "minor", "note": "Common for social media uploads, screenshots, and edited images. NOT evidence of AI generation." }) else: # Check for AI software signatures ai_software = _check_ai_software(software) if ai_software: suspicious_flags.append({ "flag": "AI_SOFTWARE", "description": f"Software signature suggests AI tool: {ai_software}", "weight": "moderate", "note": "Software tags can be stripped or spoofed. Corroborating only." }) # Check for editing software editing_software = _check_editing_software(software) if editing_software: suspicious_flags.append({ "flag": "EDITING_SOFTWARE", "description": f"Edited with: {editing_software}", "weight": "minor", "note": "Legitimate editing is common. Not evidence of fakery." }) # Standard generator dimensions if is_standard_square: suspicious_flags.append({ "flag": "STANDARD_DIMENSIONS", "description": f"Dimensions {dimensions} match common AI generator defaults", "weight": "minor", "note": "Many real photos are also square or standard sizes. Weak signal alone." }) # Missing camera hardware info (when EXIF exists but no camera data) if not camera_model and has_exif: suspicious_flags.append({ "flag": "NO_CAMERA_INFO", "description": "EXIF present but no camera make/model recorded", "weight": "minor", "note": "Some cameras/devices don't write full EXIF. Common in edited images." }) # Summary if not has_exif: summary = "No EXIF metadata available. This is normal for images from social media, messaging apps, or screenshots. Metadata absence is not evidence of manipulation." elif ai_software: summary = f"Metadata indicates creation with {ai_software}. This is a moderate corroborating signal for AI generation." elif editing_software: summary = f"Image edited with {editing_software}. Legitimate editing is common; this does not indicate fakery." elif is_standard_square: summary = f"Image has standard dimensions ({dimensions}) but camera metadata is present. Weak signal." else: summary = f"Camera metadata present: {camera_model or 'unknown device'}. Consistent with authentic capture." return { "has_exif": has_exif, "camera_model": camera_model, "software": software, "gps_present": gps_info is not None, "dimensions": dimensions, "is_standard_square": is_standard_square, "suspicious_flags": suspicious_flags, "summary": summary, "weight": "minor", # EXPLICIT: metadata is minor corroborating signal only "metadata_absent": not has_exif, # Explicit flag for downstream use } def _empty_metadata(reason: str) -> Dict[str, Any]: return { "has_exif": False, "camera_model": None, "software": None, "gps_present": False, "dimensions": "unknown", "is_standard_square": False, "suspicious_flags": [{ "flag": "EXTRACTION_FAILED", "description": reason, "weight": "none", "note": "Metadata extraction failed; no signal available." }], "summary": f"Metadata extraction failed: {reason}", "weight": "minor", "metadata_absent": True, } def _extract_exif(image: Image.Image) -> Dict: """Extract EXIF data using piexif for robust parsing.""" exif = {} try: if "exif" in image.info: exif_dict = piexif.load(image.info["exif"]) for ifd_name in ["0th", "Exif", "GPS", "1st", "Interop"]: if ifd_name in exif_dict: for tag, value in exif_dict[ifd_name].items(): try: exif[tag] = value except: pass except Exception: pass return exif def _get_camera_model(exif: Dict) -> Optional[str]: """Extract camera make/model from EXIF.""" make = exif.get(271) or exif.get(272) # Make/Model model = exif.get(272) or exif.get(271) if make and model: return f"{make} {model}".strip() return make or model def _get_software(exif: Dict) -> Optional[str]: """Extract software tag from EXIF.""" # Tag 305 = Software software = exif.get(305) if isinstance(software, bytes): software = software.decode('utf-8', errors='ignore') return software def _get_gps(exif: Dict) -> Optional[Dict]: """Extract GPS info if present.""" gps_tags = [k for k in exif.keys() if 'gps' in str(k).lower() or k in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36]] if gps_tags: return {"tags_present": len(gps_tags)} return None def _is_standard_square(width: int, height: int) -> bool: """Check if dimensions match common AI generator defaults.""" standard_sizes = { (512, 512), (768, 768), (1024, 1024), (2048, 2048), (512, 768), (768, 512), (768, 1024), (1024, 768), (896, 896), (1152, 1152), } return (width, height) in standard_sizes or (height, width) in standard_sizes def _check_ai_software(software: Optional[str]) -> Optional[str]: """Check for AI generation software signatures.""" if not software: return None software_lower = software.lower() ai_signatures = { "midjourney": "Midjourney", "stable diffusion": "Stable Diffusion", "stablediffusion": "Stable Diffusion", "dall-e": "DALL-E", "dalle": "DALL-E", "novelai": "NovelAI", "comfyui": "ComfyUI", "automatic1111": "Automatic1111", "invokeai": "InvokeAI", "krita-ai": "Krita AI", "photoshop ai": "Photoshop AI", "firefly": "Adobe Firefly", "flux": "Flux", } for key, name in ai_signatures.items(): if key in software_lower: return name return None def _check_editing_software(software: Optional[str]) -> Optional[str]: """Check for traditional editing software.""" if not software: return None software_lower = software.lower() editing_signatures = { "photoshop": "Adobe Photoshop", "lightroom": "Adobe Lightroom", "gimp": "GIMP", "affinity": "Affinity Photo", "paint.net": "Paint.NET", "snapseed": "Snapseed", "vsco": "VSCO", "lightroom": "Lightroom", } for key, name in editing_signatures.items(): if key in software_lower: return name return None