commit b079c9bf8fb70286341a4159d28f1fd02611ff8c Author: Shawn Date: Mon May 25 00:32:16 2026 -0400 EXIF scanner: multi-photo gallery with GPS detection diff --git a/__pycache__/server.cpython-311.pyc b/__pycache__/server.cpython-311.pyc new file mode 100644 index 0000000..7752140 Binary files /dev/null and b/__pycache__/server.cpython-311.pyc differ diff --git a/server.py b/server.py new file mode 100644 index 0000000..90904f1 --- /dev/null +++ b/server.py @@ -0,0 +1,144 @@ +"""EXIF + OCR test backend — validate that GPS survives upload pipeline.""" +import io, json, re, uuid +from pathlib import Path + +from fastapi import FastAPI, File, Form, HTTPException, UploadFile +from fastapi.staticfiles import StaticFiles +from PIL import Image as PILImage + +try: + import pytesseract + HAS_TESSERACT = True +except ImportError: + HAS_TESSERACT = False + +UPLOADS = Path(__file__).parent / "uploads" +UPLOADS.mkdir(exist_ok=True) + +app = FastAPI(title="EXIF Test") + + +def _dms_to_decimal(dms, ref): + """Convert EXIF DMS tuple to decimal degrees.""" + try: + deg, minutes, sec = float(dms[0]), float(dms[1]), float(dms[2]) + decimal = deg + minutes / 60.0 + sec / 3600.0 + if ref in ("S", "W"): + decimal = -decimal + return round(decimal, 7) + except Exception: + return None + + +def extract_exif(image_bytes: bytes) -> dict: + """Pull all useful EXIF fields + GPS from raw image bytes.""" + result = {"has_exif": False, "tags": {}, "gps": None} + try: + img = PILImage.open(io.BytesIO(image_bytes)) + exif = img.getexif() + if not exif: + return result + + # Standard EXIF tags + for tag_id, value in exif.items(): + tag_name = PILImage.ExifTags.TAGS.get(tag_id, f"0x{tag_id:04x}") + # Skip binary/thumbnail blobs + if tag_name in ("MakerNote", "UserComment", "PrintImageMatching"): + continue + result["tags"][tag_name] = str(value)[:300] + + if result["tags"]: + result["has_exif"] = True + + # GPS IFD + gps_ifd = exif.get_ifd(0x8825) + if gps_ifd: + lat_ref = gps_ifd.get(1, "N") + lat_dms = gps_ifd.get(2) + lng_ref = gps_ifd.get(3, "E") + lng_dms = gps_ifd.get(4) + + if lat_dms and lng_dms: + lat = _dms_to_decimal(lat_dms, lat_ref) + lng = _dms_to_decimal(lng_dms, lng_ref) + if lat is not None and lng is not None: + result["gps"] = { + "lat": lat, + "lng": lng, + "lat_ref": str(lat_ref), + "lng_ref": str(lng_ref), + "raw_lat_dms": [float(v) for v in lat_dms], + "raw_lng_dms": [float(v) for v in lng_dms], + } + except Exception as e: + result["error"] = str(e) + + return result + + +def run_ocr(image_bytes: bytes) -> dict: + """Run Tesseract OCR on the image.""" + if not HAS_TESSERACT: + return {"available": False, "text": "", "error": "pytesseract not installed"} + + tmp_path = UPLOADS / f"ocr_{uuid.uuid4().hex}.jpg" + tmp_path.write_bytes(image_bytes) + try: + img = PILImage.open(tmp_path) + img_gray = img.convert("L") + text = pytesseract.image_to_string(img_gray, config="--psm 6") + # Extract machine ID patterns + match_5dash = re.search(r"(\d{5})[-\s]*(\d{6,})", text) + match_5plus = re.search(r"(\d{5,})", text) + return { + "available": True, + "raw_text": text.strip()[:500], + "match_5dash6": match_5dash.group(0) if match_5dash else None, + "match_5plus": match_5plus.group(0) if match_5plus else None, + } + finally: + tmp_path.unlink(missing_ok=True) + + +@app.post("/api/analyze") +async def analyze_photo(file: UploadFile = File(...)): + """Upload a photo, get back EXIF + OCR results.""" + contents = await file.read() + file_size = len(contents) + + # Save + ext = Path(file.filename or "photo.jpg").suffix.lower() + if ext not in {".jpg", ".jpeg", ".png", ".webp", ".bmp", ".tiff", ".tif", ".dng"}: + ext = ".jpg" + fname = f"{uuid.uuid4().hex}{ext}" + (UPLOADS / fname).write_bytes(contents) + + exif_result = extract_exif(contents) + ocr_result = run_ocr(contents) if HAS_TESSERACT else {"available": False, "text": ""} + + return { + "filename": file.filename, + "saved_as": fname, + "file_size": file_size, + "file_size_kb": round(file_size / 1024, 1), + "exif": exif_result, + "ocr": ocr_result, + } + + +@app.get("/api/uploads") +async def list_uploads(): + """List previously uploaded files.""" + files = sorted(UPLOADS.glob("*"), key=lambda p: p.stat().st_mtime, reverse=True) + return [ + {"name": f.name, "size_kb": round(f.stat().st_size / 1024, 1)} + for f in files[:20] + ] + + +# Mount static LAST +app.mount("/", StaticFiles(directory="static", html=True), name="static") + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8903) diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..2df721e --- /dev/null +++ b/static/index.html @@ -0,0 +1,454 @@ + + + + + +EXIF Scanner — Find GPS Photos + + + + + +

📸 EXIF Scanner

+

Select multiple photos — instantly see which have GPS

+ +
+ 📱 + Tap to Select Photos + Pick from gallery — multi-select enabled +
+ + + +
+
+
0
Total
+
0
📍 Have GPS
+
0
⚠️ No GPS
+
+
+ + +
+ All + 📍 GPS + ⚠️ No GPS +
+ + + + + +
+ +
+
+ +
+
+ + + + +
+ +
+ + + + diff --git a/uploads/46f86a948f7b427d8b7f2d6681d73179.jpg b/uploads/46f86a948f7b427d8b7f2d6681d73179.jpg new file mode 100644 index 0000000..1b14abb Binary files /dev/null and b/uploads/46f86a948f7b427d8b7f2d6681d73179.jpg differ diff --git a/uploads/720c7ae9fec14a13a8729abde83bee30.jpg b/uploads/720c7ae9fec14a13a8729abde83bee30.jpg new file mode 100644 index 0000000..c18b408 Binary files /dev/null and b/uploads/720c7ae9fec14a13a8729abde83bee30.jpg differ diff --git a/uploads/7bd95d92ab3a464bb0f83d6e8ec61ee5.jpg b/uploads/7bd95d92ab3a464bb0f83d6e8ec61ee5.jpg new file mode 100644 index 0000000..9880972 Binary files /dev/null and b/uploads/7bd95d92ab3a464bb0f83d6e8ec61ee5.jpg differ diff --git a/uploads/a099b3c03dfc47529a10d2470930e3ed.jpg b/uploads/a099b3c03dfc47529a10d2470930e3ed.jpg new file mode 100644 index 0000000..1b14abb Binary files /dev/null and b/uploads/a099b3c03dfc47529a10d2470930e3ed.jpg differ diff --git a/uploads/e897232562934f1ba3404503cbc2d818.jpg b/uploads/e897232562934f1ba3404503cbc2d818.jpg new file mode 100644 index 0000000..1b14abb Binary files /dev/null and b/uploads/e897232562934f1ba3404503cbc2d818.jpg differ