Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions src/smartem_backend/api_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -2789,6 +2789,41 @@ async def get_gridsquare_image(
return await _cached_image_response(Path(gridsquare.image_path), None)


@app.get(
"/micrographs/{micrograph_uuid}/micrograph_image",
responses={200: {"content": {"image/jpeg": {}, "image/png": {}}}},
)
async def get_micrograph_image(
micrograph_uuid: str,
x: int | None = None,
y: int | None = None,
w: int | None = None,
h: int | None = None,
db: AsyncSession = DB_DEPENDENCY,
):
"""Serve the motion-corrected micrograph preview image (ADR 0021).

Prefers the pre-downscaled JPEG snapshot (served as-is, no render), falls back
to rendering the motion-corrected MRC to PNG with an optional x,y,w,h crop, and
otherwise 404s. Never serves high_res_path, which is the raw movie stack.
"""
micrograph = (await db.execute(select(Micrograph).where(Micrograph.uuid == micrograph_uuid))).scalars().first()
if not micrograph:
raise HTTPException(status_code=404, detail="Micrograph not found")
snapshot_path = micrograph.motion_corrected_snapshot_path
if snapshot_path and Path(snapshot_path).exists():
return FileResponse(
snapshot_path,
media_type="image/jpeg",
headers={"Cache-Control": "private, max-age=3600"},
)
image_path = micrograph.motion_corrected_image_path
if image_path and Path(image_path).exists():
crop = (x, y, w, h) if x is not None and y is not None and w is not None and h is not None else None
return await _cached_image_response(Path(image_path), crop)
raise HTTPException(status_code=404, detail="Micrograph image unavailable")


_frontend_sse_connections = 0
_frontend_sse_max_connections = int(os.getenv("FRONTEND_SSE_MAX_CONNECTIONS", "50"))

Expand Down
58 changes: 58 additions & 0 deletions tests/smartem_backend/test_micrograph_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""TestClient coverage for GET /micrographs/{uuid}/micrograph_image (ADR 0021, #308).

Serves the motion-corrected preview: JPEG snapshot first, MRC render fallback,
else 404. Never touches high_res_path.
"""

from .conftest import set_db_row


class TestMicrographImage:
def test_404_when_micrograph_missing(self, client):
set_db_row(client, None)
resp = client.get("/micrographs/missing/micrograph_image")
assert resp.status_code == 404
assert resp.json()["detail"] == "Micrograph not found"

def test_404_when_no_image_paths(self, client):
from smartem_backend.model.database import Micrograph

set_db_row(client, Micrograph(uuid="mic-1"))
resp = client.get("/micrographs/mic-1/micrograph_image")
assert resp.status_code == 404
assert resp.json()["detail"] == "Micrograph image unavailable"

def test_serves_snapshot_and_prefers_it_over_mrc(self, client, tmp_path):
from smartem_backend.model.database import Micrograph

snapshot = tmp_path / "snap.jpeg"
snapshot.write_bytes(b"\xff\xd8\xff\xe0jpeg-bytes")
set_db_row(
client,
Micrograph(
uuid="mic-1",
motion_corrected_snapshot_path=str(snapshot),
motion_corrected_image_path="/does/not/exist/mc.mrc",
),
)
resp = client.get("/micrographs/mic-1/micrograph_image")
assert resp.status_code == 200
assert resp.headers["content-type"] == "image/jpeg"
assert resp.content == b"\xff\xd8\xff\xe0jpeg-bytes"

def test_renders_mrc_when_snapshot_absent(self, client, tmp_path, monkeypatch):
import mrcfile
import numpy as np

from smartem_backend import api_server
from smartem_backend.model.database import Micrograph

monkeypatch.setattr(api_server, "IMAGE_CACHE_DIR", tmp_path / "cache")
source = tmp_path / "mc.mrc"
with mrcfile.new(str(source)) as mrc:
mrc.set_data(np.arange(16, dtype=np.float32).reshape(4, 4))
set_db_row(client, Micrograph(uuid="mic-1", motion_corrected_image_path=str(source)))
resp = client.get("/micrographs/mic-1/micrograph_image")
assert resp.status_code == 200
assert resp.headers["content-type"] == "image/png"
assert resp.content[:8] == b"\x89PNG\r\n\x1a\n"