Add M7: REST API server (ri-agent) with headless mode

Axum-based REST API that mirrors Tauri IPC commands, sharing the same
ImageStore via Arc. Endpoints for image open/list/close/save, stats/histogram/
pixel inspection, operations (invert, blur, threshold, edge detection, FFT,
projections, etc.), undo, and PNG tile rendering. Auto-starts on port 8787
alongside Tauri GUI. Standalone headless binary via `cargo run --bin ri-headless`.
CORS enabled for browser-based clients. 75 tests passing.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Omar Sobh
2026-03-09 23:51:55 -07:00
co-authored by Claude Opus 4.6
parent 14ba65851a
commit 432554a258
16 changed files with 917 additions and 3 deletions
+53
View File
@@ -0,0 +1,53 @@
use axum::extract::{self, State};
use axum::http::{header, StatusCode};
use axum::response::{IntoResponse, Response};
use ri_lut::Lut;
use ri_render::CompositeChannel;
use ri_types::ImageId;
use crate::error::ApiError;
use crate::state::AppState;
/// GET /images/:id/tile/:c/:z/:t/:x/:y/:w/:h -> image/png
pub async fn get_tile(
State(state): State<AppState>,
extract::Path((id, _c, z, t, x, y, w, h)): extract::Path<(String, u32, u32, u32, u32, u32, u32, u32)>,
) -> Result<Response, ApiError> {
let image_id: ImageId = id.parse().map_err(|e: uuid::Error| ApiError::bad_request(e.to_string()))?;
let store = state.store.clone();
let png_bytes = tokio::task::spawn_blocking(move || {
store.with_entry(&image_id, |entry| {
let hs = &entry.hyperstack;
let img_w = hs.width();
let img_h = hs.height();
let visible: Vec<_> = entry
.channel_states
.iter()
.filter(|cs| cs.visible)
.filter_map(|cs| {
let plane = hs.get_plane(cs.channel_index, z, t)?;
Some(CompositeChannel {
plane,
lut: Lut::from_channel_color(&cs.color),
display_range: &cs.display_range,
})
})
.collect();
ri_render::render_composite_tile_png(&visible, img_w, img_h, x, y, w, h)
})
.map_err(|e| ApiError::not_found(e.to_string()))?
.map_err(|e| ApiError::internal(e.to_string()))
})
.await??;
Ok((
StatusCode::OK,
[(header::CONTENT_TYPE, "image/png")],
png_bytes,
)
.into_response())
}