Spaces:
Sleeping
Sleeping
davanstrien HF Staff
Mirador + IIIF Image API + Content Search over Surya OCR lines
ecd2a6a verified | import io, json | |
| from fastapi import FastAPI, Request, Query | |
| from fastapi.responses import JSONResponse, HTMLResponse, Response | |
| from PIL import Image | |
| app = FastAPI() | |
| IMG = Image.open("page.jpg").convert("RGB") | |
| W, H = IMG.size | |
| LINES = [l for l in json.load(open("lines.json")) if l.get("text")] | |
| CORS = {"Access-Control-Allow-Origin": "*"} | |
| def base(req): return str(req.base_url).rstrip("/") | |
| def canvas(b): return f"{b}/canvas/p1" | |
| # ---- minimal dynamic IIIF Image API 2.1 (level1) ---- | |
| def parse_region(r): | |
| if r in ("full", "square"): return (0, 0, W, H) | |
| if r.startswith("pct:"): | |
| x, y, w, h = [float(v) for v in r[4:].split(",")] | |
| return (int(x/100*W), int(y/100*H), int(w/100*W), int(h/100*H)) | |
| x, y, w, h = [int(float(v)) for v in r.split(",")] | |
| return (x, y, w, h) | |
| def parse_size(s, rw, rh): | |
| if s in ("full", "max", "^max"): return (rw, rh) | |
| if s.startswith("pct:"): | |
| p = float(s[4:]) / 100; return (max(1, int(rw*p)), max(1, int(rh*p))) | |
| fit = s.startswith("!"); s = s[1:] if fit else s | |
| w, h = (s.split(",") + [""])[:2] | |
| if w and not h: w = int(w); return (w, max(1, round(rh*w/rw))) | |
| if h and not w: h = int(h); return (max(1, round(rw*h/rh)), h) | |
| w, h = int(w), int(h) | |
| if fit: | |
| sc = min(w/rw, h/rh); return (max(1, int(rw*sc)), max(1, int(rh*sc))) | |
| return (w, h) | |
| def info(req: Request): | |
| b = base(req) | |
| return JSONResponse({"@context": "http://iiif.io/api/image/2/context.json", | |
| "@id": f"{b}/iiif/page", "@type": "iiif:Image", | |
| "protocol": "http://iiif.io/api/image", "width": W, "height": H, | |
| "profile": ["http://iiif.io/api/image/2/level1.json"], | |
| "tiles": [{"width": 1024, "scaleFactors": [1, 2, 4, 8, 16, 32]}]}, headers=CORS) | |
| def tile(region: str, size: str, rotation: str, quality: str, fmt: str): | |
| x, y, w, h = parse_region(region) | |
| crop = IMG.crop((x, y, x + w, y + h)) | |
| tw, th = parse_size(size, w, h) | |
| crop = crop.resize((max(1, tw), max(1, th))) | |
| if quality == "gray": crop = crop.convert("L").convert("RGB") | |
| buf = io.BytesIO(); crop.save(buf, "JPEG", quality=85) | |
| return Response(buf.getvalue(), media_type="image/jpeg", headers=CORS) | |
| # ---- Presentation 3.0 manifest (image service + OCR annotations + search service) ---- | |
| def manifest(req: Request): | |
| b = base(req); c = canvas(b) | |
| anns = [] | |
| for i, ln in enumerate(LINES): | |
| x0, y0, x1, y1 = [int(round(v)) for v in ln["bbox"]] | |
| anns.append({"id": f"{b}/anno/l{i}", "type": "Annotation", "motivation": "supplementing", | |
| "body": {"type": "TextualBody", "format": "text/plain", "language": "en", "value": ln["text"]}, | |
| "target": f"{c}#xywh={x0},{y0},{max(1,x1-x0)},{max(1,y1-y0)}"}) | |
| m = {"@context": "http://iiif.io/api/presentation/3/context.json", | |
| "id": f"{b}/manifest.json", "type": "Manifest", | |
| "label": {"en": ["The Commoner, 1901-01-23, p.1 — Surya OCR 2 (line-level)"]}, | |
| "service": [{"id": f"{b}/search", "type": "SearchService1", | |
| "profile": "http://iiif.io/api/search/1/search"}], | |
| "items": [{"id": c, "type": "Canvas", "width": W, "height": H, | |
| "items": [{"id": f"{c}/page", "type": "AnnotationPage", "items": [ | |
| {"id": f"{c}/painting", "type": "Annotation", "motivation": "painting", | |
| "body": {"id": f"{b}/iiif/page/full/max/0/default.jpg", "type": "Image", | |
| "format": "image/jpeg", "width": W, "height": H, | |
| "service": [{"id": f"{b}/iiif/page", "type": "ImageService2", "profile": "level1"}]}, | |
| "target": c}]}], | |
| "annotations": [{"id": f"{c}/ocr", "type": "AnnotationPage", "items": anns}]}]} | |
| return JSONResponse(m, headers=CORS) | |
| # ---- IIIF Content Search 1.0 (Mirador's search box queries this) ---- | |
| def search(req: Request, q: str = Query("")): | |
| b = base(req); c = canvas(b); ql = q.lower().strip() | |
| res = [] | |
| for i, ln in enumerate(LINES): | |
| if ql and ql in ln["text"].lower(): | |
| x0, y0, x1, y1 = [int(round(v)) for v in ln["bbox"]] | |
| res.append({"@id": f"{b}/anno/s{i}", "@type": "oa:Annotation", "motivation": "sc:painting", | |
| "resource": {"@type": "cnt:ContentAsText", "chars": ln["text"]}, | |
| "on": f"{c}#xywh={x0},{y0},{max(1,x1-x0)},{max(1,y1-y0)}"}) | |
| return JSONResponse({"@context": "http://iiif.io/api/presentation/2/context.json", | |
| "@id": f"{b}/search?q={q}", "@type": "sc:AnnotationList", | |
| "within": {"@type": "sc:Layer", "total": len(res)}, "resources": res}, headers=CORS) | |
| def index(): | |
| return """<!DOCTYPE html><html><head><meta charset="utf-8"> | |
| <title>Surya OCR 2 — IIIF search (Mirador)</title> | |
| <style>html,body,#m{height:100%;width:100%;margin:0}</style></head> | |
| <body><div id="m"></div> | |
| <script src="https://unpkg.com/mirador@3.3.0/dist/mirador.min.js"></script> | |
| <script> | |
| Mirador.viewer({ id:'m', | |
| window:{ allowFullscreen:true, sideBarOpenByDefault:true, defaultSideBarPanel:'search' }, | |
| windows:[{ manifestId: location.origin + '/manifest.json' }] }); | |
| </script></body></html>""" | |