CoolFace
Apppublic

evoneural/evoneuralIn3D-app

sourceHugging Faceapache-2.0updated 8mo agoView on Hugging Face
0likes
panorama_viewer.py160 linesDownload Raw Back to scripts
1"""2360° panorama viewer for equirectangular (2:1) skybox images.3Returns HTML for use with st.components.v1.html().4Resizes image to keep data URL small so it loads reliably in iframes.5Fetches Pannellum script server-side and inlines it so the client does not load from CDN (avoids CSP/network block).6"""7 8import base649import io10import urllib.request11from pathlib import Path12 13# Max width for viewer image (keeps data URL under ~1MB for reliable loading)14MAX_VIEWER_WIDTH = 102415 16# CDN URLs for Pannellum (fetched server-side and inlined)17PANNELLUM_JS_URL = "https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.js"18PANNELLUM_CSS_URL = "https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.css"19 20# Optional local fallback (if CDN is blocked on server)21_SCRIPT_DIR = Path(__file__).resolve().parent22_PANNELLUM_ASSETS = _SCRIPT_DIR / "panorama_assets"23 24 25def _resize_and_encode(image_path: Path) -> tuple[str, str]:26    """Load image, resize to max width (keep 2:1), return (data_url, mime)."""27    from PIL import Image28 29    img = Image.open(image_path).convert("RGB")30    w, h = img.size31    if w > MAX_VIEWER_WIDTH:32        new_w = MAX_VIEWER_WIDTH33        new_h = max(256, (new_w * h) // w)34        img = img.resize((new_w, new_h), Image.Resampling.LANCZOS)35    buf = io.BytesIO()36    img.save(buf, format="JPEG", quality=85)37    b64 = base64.b64encode(buf.getvalue()).decode("utf-8")38    return f"data:image/jpeg;base64,{b64}", "image/jpeg"39 40 41def _fetch_pannellum_assets() -> tuple[str | None, str | None]:42    """Fetch or read Pannellum JS and CSS. Returns (js_content, css_content) or (None, None) on failure."""43    js_content, css_content = None, None44 45    # Try local assets first (no network)46    js_file = _PANNELLUM_ASSETS / "pannellum.js"47    css_file = _PANNELLUM_ASSETS / "pannellum.css"48    if js_file.exists() and css_file.exists():49        js_content = js_file.read_text(encoding="utf-8", errors="replace")50        css_content = css_file.read_text(encoding="utf-8", errors="replace")51        return js_content, css_content52 53    # Fetch from CDN (server-side)54    try:55        req = urllib.request.Request(PANNELLUM_JS_URL, headers={"User-Agent": "Mozilla/5.0"})56        with urllib.request.urlopen(req, timeout=10) as r:57            js_content = r.read().decode("utf-8", errors="replace")58    except Exception:59        js_content = None60    try:61        req = urllib.request.Request(PANNELLUM_CSS_URL, headers={"User-Agent": "Mozilla/5.0"})62        with urllib.request.urlopen(req, timeout=10) as r:63            css_content = r.read().decode("utf-8", errors="replace")64    except Exception:65        css_content = None66 67    return js_content, css_content68 69 70def image_to_data_url(image_path: str | Path) -> str:71    """Read image file, resize if needed, return a data URL (base64)."""72    path = Path(image_path)73    if not path.exists():74        return ""75    data_url, _ = _resize_and_encode(path)76    return data_url77 78 79def panorama_html(80    image_path: str | Path,81    height_px: int = 480,82    full_page_background: bool = False,83) -> str:84    """85    Build HTML for an interactive 360° panorama viewer (Pannellum).86    image_path: path to equirectangular 2:1 image (e.g. skybox PNG).87    height_px: viewer height in pixels (ignored if full_page_background=True).88    full_page_background: if True, viewer fills 100% of container (use as page background).89    Pannellum JS/CSS are fetched server-side and inlined so the client does not load from CDN.90    """91    path = Path(image_path)92    if not path.exists():93        return f'<p style="padding:1em;color:#888;">Image not found: {path.name}</p>'94    if not path.is_file():95        return f'<p style="padding:1em;color:#888;">Not a file: {path.name}</p>'96 97    js_content, css_content = _fetch_pannellum_assets()98    if not js_content or not css_content:99        return (100            '<p style="padding:1em;color:#c66;">Viewer unavailable: could not load Pannellum. '101            "Check network or add scripts/panorama_assets/pannellum.js and pannellum.css.</p>"102        )103 104    try:105        data_url, _ = _resize_and_encode(path)106    except Exception:107        return '<p style="padding:1em;color:#c66;">Could not load image (corrupted or invalid format).</p>'108    data_url_escaped = data_url.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "")109 110    # Inline script: escape </script> so it does not close our tag111    js_safe = js_content.replace("</script>", "<\\/script>")112 113    if full_page_background:114        size_style = "html, body { margin: 0; padding: 0; width: 100%; height: 100%; }\n    #panorama { width: 100%; height: 100%; min-height: 100vh; }"115    else:116        size_style = f"body {{ margin: 0; }}\n    #panorama {{ width: 100%; height: {height_px}px; }}"117 118    return f"""119<!DOCTYPE html>120<html>121<head>122  <meta charset="utf-8">123  <meta name="viewport" content="width=device-width, initial-scale=1">124  <style>125    {size_style}126    .pnlm-container {{ border-radius: 0; }}127    .pnlm-error {{ color: #ccc; padding: 1em; }}128  </style>129  <style>{css_content}</style>130</head>131<body>132  <div id="panorama"></div>133  <script>{js_safe}</script>134  <script>135    (function() {{136      var panoramaUrl = "{data_url_escaped}";137      try {{138        pannellum.viewer('panorama', {{139          type: 'equirectangular',140          panorama: panoramaUrl,141          autoLoad: true,142          showControls: true,143          compass: true,144          mouseZoom: true,145          draggable: true,146          showZoomCtrl: true,147          showFullscreenCtrl: true,148          hfov: 100,149          minHfov: 50,150          maxHfov: 120151        }});152      }} catch (e) {{153        document.getElementById('panorama').innerHTML = '<p class="pnlm-error">Viewer error: ' + e.message + '</p>';154      }}155    }})();156  </script>157</body>158</html>159"""160