2024-08-01 21:41:20 +00:00
|
|
|
use crate::display::{create_display_thread, EInkPanel};
|
2024-08-01 01:20:22 +00:00
|
|
|
use crate::dither::{DitherMethod, DitheredImage};
|
|
|
|
use crate::eink::Palette;
|
2024-07-30 13:53:47 +00:00
|
|
|
use axum::async_trait;
|
|
|
|
use axum::extract::{FromRequest, Multipart, State};
|
2024-07-30 05:35:43 +00:00
|
|
|
use axum::http::{header, StatusCode};
|
2024-07-27 19:11:44 +00:00
|
|
|
use axum::response::IntoResponse;
|
2024-07-30 13:53:47 +00:00
|
|
|
use axum::{response::Response, routing::post, Router};
|
2024-08-02 02:15:13 +00:00
|
|
|
use image::{imageops::resize, imageops::FilterType, ImageReader, RgbImage};
|
2024-07-30 13:53:47 +00:00
|
|
|
use std::io::Cursor;
|
2024-07-30 05:35:43 +00:00
|
|
|
use std::str;
|
|
|
|
use std::str::FromStr;
|
2024-08-01 21:41:20 +00:00
|
|
|
use std::sync::mpsc;
|
2024-07-28 04:01:57 +00:00
|
|
|
use std::sync::Arc;
|
2024-08-01 21:41:20 +00:00
|
|
|
use std::thread::JoinHandle;
|
2024-07-30 14:20:18 +00:00
|
|
|
use tracing::{error, info, instrument};
|
2024-07-18 12:34:28 +00:00
|
|
|
|
2024-07-30 05:35:43 +00:00
|
|
|
#[derive(thiserror::Error, Debug)]
|
|
|
|
pub enum ApiError {
|
|
|
|
#[error("missing image field")]
|
|
|
|
MissingImage,
|
2024-07-27 19:11:44 +00:00
|
|
|
}
|
2024-07-24 14:43:09 +00:00
|
|
|
|
|
|
|
#[derive(Clone)]
|
2024-07-31 18:31:35 +00:00
|
|
|
pub struct AppState {
|
2024-08-01 21:41:20 +00:00
|
|
|
display_channel: mpsc::Sender<Box<DitheredImage>>,
|
2024-07-28 04:01:57 +00:00
|
|
|
display_task: Arc<JoinHandle<()>>,
|
2024-07-27 19:11:44 +00:00
|
|
|
}
|
2024-07-28 04:01:57 +00:00
|
|
|
|
2024-07-31 18:31:35 +00:00
|
|
|
impl AppState {
|
2024-07-27 19:11:44 +00:00
|
|
|
#[must_use]
|
2024-07-28 04:01:57 +00:00
|
|
|
pub fn new(disp: Box<dyn EInkPanel + Send>) -> Self {
|
2024-08-01 21:41:20 +00:00
|
|
|
let (handle, tx) = create_display_thread(disp);
|
2024-07-27 19:11:44 +00:00
|
|
|
Self {
|
2024-07-28 04:01:57 +00:00
|
|
|
display_channel: tx,
|
2024-08-01 21:41:20 +00:00
|
|
|
display_task: Arc::new(handle),
|
2024-07-28 04:01:57 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-27 19:11:44 +00:00
|
|
|
// Make our own error that wraps `anyhow::Error`.
|
|
|
|
struct AppError(anyhow::Error);
|
|
|
|
|
|
|
|
// Tell axum how to convert `AppError` into a response.
|
|
|
|
impl IntoResponse for AppError {
|
|
|
|
fn into_response(self) -> Response {
|
|
|
|
(
|
|
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
|
|
format!("Something went wrong: {}", self.0),
|
|
|
|
)
|
|
|
|
.into_response()
|
|
|
|
}
|
2024-07-24 14:43:09 +00:00
|
|
|
}
|
2024-07-27 19:11:44 +00:00
|
|
|
|
|
|
|
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
|
|
|
|
// `Result<_, AppError>`. That way you don't need to do that manually.
|
|
|
|
impl<E> From<E> for AppError
|
|
|
|
where
|
|
|
|
E: Into<anyhow::Error>,
|
|
|
|
{
|
|
|
|
fn from(err: E) -> Self {
|
|
|
|
Self(err.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-18 12:34:28 +00:00
|
|
|
/// API routes for axum
|
|
|
|
/// Start with the basics: Send an image, crop it, dither, and upload.
|
|
|
|
/// we defer the upload to a separate task.
|
2024-07-31 18:31:35 +00:00
|
|
|
pub fn router() -> Router<AppState> {
|
2024-07-29 17:51:51 +00:00
|
|
|
Router::new()
|
|
|
|
.route("/setimage", post(set_image))
|
2024-07-30 05:35:43 +00:00
|
|
|
.route("/preview", post(preview_image))
|
2024-07-29 17:51:51 +00:00
|
|
|
}
|
|
|
|
|
2024-07-29 21:39:49 +00:00
|
|
|
#[derive(Debug)]
|
2024-07-30 05:35:43 +00:00
|
|
|
struct ImageRequest {
|
|
|
|
image: Box<RgbImage>,
|
|
|
|
dither_method: DitherMethod,
|
2024-07-31 02:46:15 +00:00
|
|
|
palette: Palette,
|
2024-07-30 05:35:43 +00:00
|
|
|
}
|
|
|
|
|
2024-07-30 13:53:47 +00:00
|
|
|
#[async_trait]
|
|
|
|
impl<S> FromRequest<S> for ImageRequest
|
|
|
|
where
|
|
|
|
S: Send + Sync,
|
|
|
|
{
|
|
|
|
type Rejection = AppError;
|
|
|
|
|
|
|
|
async fn from_request(req: axum::extract::Request, state: &S) -> Result<Self, Self::Rejection> {
|
|
|
|
let mut parts = Multipart::from_request(req, state).await?;
|
2024-07-30 05:35:43 +00:00
|
|
|
let mut img = None;
|
|
|
|
let mut palette = None;
|
|
|
|
let mut dither_method = None;
|
|
|
|
while let Some(field) = parts.next_field().await? {
|
|
|
|
match field.name() {
|
|
|
|
Some("image") => {
|
|
|
|
let data = field.bytes().await?;
|
|
|
|
let reader = ImageReader::new(Cursor::new(data))
|
|
|
|
.with_guessed_format()
|
|
|
|
.expect("cursor never fails");
|
|
|
|
let image = reader.decode()?;
|
|
|
|
img = Some(Box::new(image.into()));
|
|
|
|
}
|
|
|
|
Some("palette") => {
|
|
|
|
let data = field.bytes().await?;
|
|
|
|
let val = str::from_utf8(&data)?;
|
2024-07-31 02:46:15 +00:00
|
|
|
palette = Some(Palette::from_str(val)?);
|
2024-07-30 05:35:43 +00:00
|
|
|
}
|
|
|
|
Some("dither_method") => {
|
|
|
|
let data = field.bytes().await?;
|
|
|
|
let val = str::from_utf8(&data)?;
|
|
|
|
dither_method = Some(DitherMethod::from_str(val)?);
|
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
2024-07-30 14:20:18 +00:00
|
|
|
img.map_or_else(
|
|
|
|
|| Err(ApiError::MissingImage.into()),
|
|
|
|
|i| {
|
|
|
|
Ok(Self {
|
|
|
|
image: i,
|
|
|
|
dither_method: dither_method.unwrap_or(DitherMethod::NearestNeighbor),
|
2024-07-31 02:46:15 +00:00
|
|
|
palette: palette.unwrap_or(Palette::Default),
|
2024-07-30 14:20:18 +00:00
|
|
|
})
|
|
|
|
},
|
|
|
|
)
|
2024-07-30 05:35:43 +00:00
|
|
|
}
|
2024-07-24 14:43:09 +00:00
|
|
|
}
|
|
|
|
|
2024-08-02 02:15:13 +00:00
|
|
|
#[instrument(skip_all)]
|
2024-07-27 19:11:44 +00:00
|
|
|
async fn set_image(
|
2024-07-31 18:31:35 +00:00
|
|
|
State(ctx): State<AppState>,
|
2024-07-30 13:53:47 +00:00
|
|
|
img_req: ImageRequest,
|
2024-07-27 19:11:44 +00:00
|
|
|
) -> Result<impl IntoResponse, AppError> {
|
2024-07-30 14:20:18 +00:00
|
|
|
// FIXME: resize image to 800x480 to match the eink panel.
|
2024-08-02 02:15:13 +00:00
|
|
|
info!("Got image");
|
|
|
|
let mut buf = DitheredImage::new(800, 480, img_req.palette.value().to_vec());
|
|
|
|
let resized = resize(&*img_req.image, 800, 480, FilterType::Lanczos3);
|
2024-07-30 05:35:43 +00:00
|
|
|
{
|
2024-07-30 13:53:47 +00:00
|
|
|
let mut dither = img_req.dither_method.get_ditherer();
|
2024-08-02 02:15:13 +00:00
|
|
|
dither.dither(&resized, &mut buf);
|
2024-07-27 19:11:44 +00:00
|
|
|
}
|
2024-08-02 02:15:13 +00:00
|
|
|
info!("image resized, pushing to channel");
|
2024-08-01 21:41:20 +00:00
|
|
|
ctx.display_channel.send(Box::new(buf))?;
|
2024-07-30 05:35:43 +00:00
|
|
|
Ok(StatusCode::OK)
|
2024-07-24 14:43:09 +00:00
|
|
|
}
|
2024-07-29 17:51:51 +00:00
|
|
|
|
2024-07-30 05:35:43 +00:00
|
|
|
/// generates a dithered image based on the given image and the dithering parameters.
|
|
|
|
/// Can be used to see how the dithering and palette choices affect the result.
|
2024-07-30 13:53:47 +00:00
|
|
|
async fn preview_image(img_req: ImageRequest) -> Result<impl IntoResponse, AppError> {
|
2024-08-02 02:15:13 +00:00
|
|
|
let mut buf = DitheredImage::new(800, 480, img_req.palette.value().to_vec());
|
|
|
|
let resized = resize(&*img_req.image, 800, 480, FilterType::Lanczos3);
|
2024-07-30 05:35:43 +00:00
|
|
|
{
|
2024-07-30 13:53:47 +00:00
|
|
|
let mut dither = img_req.dither_method.get_ditherer();
|
2024-08-02 02:15:13 +00:00
|
|
|
dither.dither(&resized, &mut buf);
|
2024-07-30 05:35:43 +00:00
|
|
|
}
|
|
|
|
// Convert buf into a png image.
|
|
|
|
let img = buf.into_rgbimage();
|
|
|
|
|
|
|
|
let mut buffer = Cursor::new(Vec::new());
|
|
|
|
img.write_to(&mut buffer, image::ImageFormat::Png)?;
|
|
|
|
|
|
|
|
let headers = [(header::CONTENT_TYPE, mime::IMAGE_PNG.to_string())];
|
|
|
|
Ok((StatusCode::OK, headers, buffer.into_inner()))
|
2024-07-29 17:51:51 +00:00
|
|
|
}
|